线程同步是JAVA多线程高并发编程的重要话题,常常用到的同步机制包括synchronized、ReentrantLock(可重入锁)、Semephore(信号灯)、CyclicBarrier(循环栅栏)、CountDownLatch(倒计时锁)等,今天对比较常用的synchronized的各种加锁方式与ReentrantLock进行性能评测。
- 目录结构
- 性能评测方案
- 性能评测结论
- 性能评测源码
- 同步机制浅析
- synchronized与Lock比较
1 性能评测方案
测评任务:用50个线程,对一个静态变量从0数到5亿
测评目的:比较synchonized与ReentrantLock的性能差异
方案思路:50个线程、加剧同步竞争;5亿次加锁、任务量次数够多;简单的+1运算、CPU消耗型任务、降低内存I/O等干扰;随机不同锁类型的执行顺序,降低不确定性因素干扰
测评次数:10次,消除随机因素影响
- 加锁设计
1 对象实例方法synchronized同步,如public synchronized void addCounter()
2 对象静态方法synchronized同步,如public static synchronized void addCounter()
3 代码块内synchronized Class<?>同步, 如synchronized(LockObject.class)
4 代码块内ReentrantLock同步
2 性能评测结论
2.1 测试结果
| 序号 | 实例方法(ms) | Class<?>(ms) | 可重入锁(ms) | 静态方法(ms) | 平均时间(ms) |
|---|---|---|---|---|---|
| 1 | 15,569 | 16,485 | 11,289 | 73,919 | 29,315 |
| 2 | 15,252 | 16,781 | 11,630 | 74,147 | 29,452 |
| 3 | 15,906 | 16,634 | 11,545 | 77,101 | 30,296 |
| 4 | 15,483 | 16,293 | 11,497 | 74,853 | 29,531 |
| 5 | 15,544 | 16,195 | 11,394 | 77,199 | 30,083 |
| 6 | 14,831 | 16,614 | 11,471 | 78,771 | 30,421 |
| 7 | 15,382 | 16,557 | 11,499 | 77,011 | 30,112 |
| 8 | 15,559 | 16,363 | 11,432 | 78,120 | 30,368 |
| 9 | 15,512 | 16,574 | 11,503 | 77,801 | 30,347 |
| 10 | 15,643 | 16,372 | 11,543 | 73,269 | 29,206 |
| 平均 | 15,468 | 16,486 | 11,480 | 76,219 | 29,913 |
2.2 测评结论
分析评测数据,经过10轮测试、总计50亿次锁同步,可重入锁ReentrantLock平均完成时间11.48秒,实例方法synchronized平均完成时间15.47秒,Class<?>类型synchronized平均完成时间16.49秒,静态方法synchronized平均完成时间76.22秒。
测评冠军:可重入锁ReentrantLock,性能优于任何形式的synchronized
性能排序:可重入锁 > 对象方法 > Class<?> 静态方法
最佳实践
可重入锁性能最优,高并发尽量采用可重入锁,尽量少在大量调用的静态方法上采用synchronized同步。
3 性能评测代码
第一步 添加测试执行类
package LockStudy.test;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestManager {
protected class LockObject {}
protected int lockType;
protected static long counter = 0;
protected long maxCounter = 0;
protected Lock lockObj = new ReentrantLock(false);
protected Date tmStart;
protected Date tmFinish;
public TestManager(int lockType){
this.lockType = lockType;
}
/**
* 启动计数工作线程
* @param initWorker 工作线程数量
* @param maxCounter 最大计数上限
* @throws Exception
*/
public void init(int initWorker, long maxCounter) throws Exception {
this.maxCounter = maxCounter;
counter = 0;
tmStart = new Date();
for(int i=0; i<initWorker; i++){
TestRunner obj = new TestRunner(this);
Thread th = new Thread(obj);
th.start();
//Thread.sleep(50);
}
}
/**
* 记录任务完成时间
*/
protected void onTaskComplete(){
tmFinish = new Date();
}
/**
* 返回任务完成时间
* @return 完成时间,单位毫秒ms
*/
public long getTimeElapsed(){
long temp = tmFinish.getTime() - tmStart.getTime();
return temp;
}
public boolean isGameOver(){
if(counter>maxCounter){
synchronized (this){
if(tmFinish==null){
onTaskComplete();
}
}
return true;
}
return (counter>maxCounter);
}
protected void addCounter(){
switch (lockType){
case 0:
addCounterWithSyn();
break;
case 1:
addCounterWithClass();
break;
case 2:
addCounterWithLock();
break;
case 3:
addCounterWithStatic();
break;
}
}
/**
* 对象方法synchronized
*/
protected synchronized void addCounterWithSyn(){
counter++;
}
/**
* 对象类型synchronized
*/
protected void addCounterWithClass(){
synchronized (LockObject.class){
counter++;
}
}
/**
* ReentrantLock同步
*/
protected void addCounterWithLock(){
lockObj.lock();
counter++;
lockObj.unlock();
}
/**
* 静态方法synchronized
*/
protected static synchronized void addCounterWithStatic(){
counter++;
}
/**
* 计数工作线程
*/
protected class TestRunner implements Runnable{
protected TestManager manager;
public TestRunner(TestManager pManager){
this.manager = pManager;
}
@Override
public void run(){
long count = 0;
while(!manager.isGameOver()){
manager.addCounter();
count++;
}
//System.out.println("ThreadId=" + Thread.currentThread().getId() + ",\tcount=" + count);
}
}
}
TestManager类的addCounter根据同步锁类型,调用四个不同加锁机制的方法,四个方法逻辑都是对counter静态变量执行+1操作。
第二步 编写测试方案主控逻辑类
package LockStudy;
import LockStudy.test.TestManager;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 同步锁性能对比
* @author 明月照我行@简书
*/
public class App
{
protected class LockObject {}
public static void main( String[] args ) throws Exception
{
System.out.println( "Test start..." );
//doTestSynWithException();
//doTestLockWithException();
int count = 0;
Random rand = new Random();
long[][] results = new long[11][5];
ArrayList lockTypeList = new ArrayList(4);
while(count<10) {
/**
* lockType:锁类型
* 0=synchronized method 1=synchronized class
* 2=ReentrantLock 3=synchronized static method
*/
for(int i=0; i<4; i++){
lockTypeList.add(i);
}
System.out.println(String.format("---第%d轮测试开始----", count+1));
while(lockTypeList.size()>0){
/**
* 随机测试顺序,确保不会因为执行顺序干扰评测结果
*/
int index = rand.nextInt(lockTypeList.size());
int lockType = (int)lockTypeList.get(index);
lockTypeList.remove(index);
long timeElapsed = doTest(lockType);
results[count][lockType] = timeElapsed;
}
System.out.println(String.format("---第%d轮测试结束----", count+1));
count++;
}
/**
* 求每一轮次平均时间消耗
*/
for(int row=0; row<10; row++){
long temp = 0;
for(int i=0; i<4; i++){
temp += results[row][i];
}
results[row][4] = temp/4;
}
/**
* 求每一类型锁平均时间消耗
*/
for(int col=0; col<5; col++){
long temp=0;
for(int i=0; i<10; i++){
temp += results[i][col];
}
results[10][col] = temp / 10;
}
/**
* 输出评测结果矩阵
*/
System.out.println( "-----------Output Results ----------" );
for(int row=0; row<11; row++){
System.out.print("|" + (row+1));
for(int i=0; i<5; i++){
System.out.print("|" + results[row][i]);
}
System.out.println("|");
System.out.println( "----------------------------------" );
}
System.out.println( "Test completed..." );
}
protected static long doTest(int lockType) throws Exception {
/**
* 50个线程,加锁5亿次
*/
int threads = 50;
long maxCounter = 5 * 10000;
maxCounter *= 10000;
TestManager myobj = new TestManager(lockType);
myobj.init(threads, maxCounter);
while (!myobj.isGameOver()) {
Thread.sleep(100);
}
long result = myobj.getTimeElapsed();
if (lockType == 0) {
System.out.println("synchronized method, time elapsed " + result + "ms");
} else if (lockType == 1) {
System.out.println("synchronized class, time elapsed " + result + "ms");
} else if (lockType == 2) {
System.out.println("reentrantlock, time elapsed " + result + "ms");
} else {
System.out.println("synchronized static method, time elapsed " + result + "ms");
}
myobj = null;
Thread.sleep(1000);
return result;
}
/**
* 测试synchronized在代码块内发生异常后是否正常释放
* @throws Exception
*/
protected static void doTestSynWithException() throws Exception {
int a = 5, b = 0;
try {
synchronized (LockObject.class) {
a = a / b;
}
}catch(Exception ex){
}
/**
* 启动新线程,锁具有可重入特性
*/
Thread thobj = new Thread(new Runnable(){
public void run(){
synchronized (LockObject.class){
System.out.println("synchronized released successfully after exception.");
}
}
});
thobj.start();
Thread.sleep(5000);
}
}
附送:doTestSynWithException函数,验证在synchronized代码块内发生异常,锁能够正常释放。
4 同步机制浅析
4.1 synchronized
分析上述代码,synchronized同步应用在对象方法、静态方法、Class<?>与对象实例上,本质上都是对JAVA对象加锁。静态方法同步是在方法所在类的类型Class<?>上加锁,对象方法同步是在该对象实例的对象头上加锁。
根据知乎网友的《Java synchronized原理总结》一文分享,synchronized通过JAVA对象头锁标记,实现从偏向锁、轻量级锁到重量级锁的升级降级来优化锁性能,重量级锁采用操作系统的Mutex-互斥锁实现。
- 优化思路
- 控制JAVA对象头锁标记,比较是否加锁以及持有锁的线程ID,对相同线程多次加锁支持可重入访问;
- 避免从JAVA虚拟机切换到系统态执行,逐步升级到重量级锁Mutex;
- 利用操作系统线程调度的CPU时间碎片分配机制,线程获得执行权后,一段时间内拥有CPU内核,这个是偏向锁升级机制的基础。
4.2 ReentrantLock
可重入锁ReentrantLock是Lock接口的一个实现,提供比synchronized更精细和扩展的锁同步功能,底层调用LockSupport的静态方法park/unpark,LockSupport类调用操作系统native本地方法实现锁同步操作。JAVA常用同步锁的源码我计划专门写一篇博文分析。
Lock锁对象,要注意异常处理,避免因异常导致锁无法正常释放,JDK推荐代码写法如下:
Lock lockObj = ...
lockObj.lock();
try{
...
}finally{
lockObj.unlock();
}
5 synchronized与Lock比较
性能:可重入锁ReentrantLock性能优于任何形式的synchronized同步。
方便性:synchronized比Lock方便,不用特地创建锁对象,可以方便在对象类型、方法、对象实例上同步。
灵活性:Lock对象高于synchronized,Lock对象实例可以作为参数传递,可以在不同代码块lock/unlock。
复杂性:synchronized简单,不用Lock对象的费心lock/unlock逻辑,同时,synchronized代码块内发生异常,同步对象的锁标记自动复位。
控制精度:Lock对象支持在加锁时指定超时等待;Lock对象支持tryLock操作,不成功及时返回。
最佳实践:1) 高并发关键代码,尽量使用Lock对象,要注意lock/unlock中间异常的处理; 2) 尽量避免在静态方法采用synchronized同步; 3) synchronized对象非静态方法或对象实例上同步方便高效,对并发度要求不高、锁逻辑复杂的地方可多食用。