Synchronized锁优化原理
JDK5之后,Synchronized实现了各种锁优化策略,如适应性自旋、锁消除、锁粗化、锁膨胀、轻量级锁、偏向锁等。
适应性自旋
CAS自旋是通过硬件实现的计算机原语来实现的
通常情况下的CAS是比较并替换,在无限循环中直至替换成功
for(;;){//compareAndSet是本地方法,底层是通过硬件实现的一组计算机原语if(compareAndSet(expect,news)){return news;}}
在早期的(JDK5及以前)JVM中,自旋锁默认是关闭的,可以通过-XX:+UseSpinning参数开启,并使用-XX:PreBlockSpin参数来自定义自旋次数,默认是10次,这些参数是全局设定的,对于不同的锁都是一样的效果。类似于下面代码
for(int i = 0; i < 10; i ++){if(compareAndSet(expect,news)){return news;}}
在JDK6之后,对自旋锁进行了优化,也就是优化成了适应性自旋锁,他的自旋次数是由上一次锁的自旋时间及锁的拥有者(持锁线程)的状态决定,如果自旋获取锁的成功率更高,那么自旋允许的次数也相对较多,可能是几十或者上百次。
如果自旋获取锁的成功率较低,说明竞争较大,同步区的执行时间较长,那么自旋次数也相对较少,改用阻塞返回更有利于性能的提升。
锁消除
锁消除顾名思义就是不使用锁,取消锁。这种情况是为了优化一些隐藏式的同步过程,一些看似没有使用锁的代码,也有可能会被编译成同步代码,例如JDK5之前,字符串的相加是会被转换成StringBuffer类来操作的
public String concatString(String s1,String s2,String s3){return s1+s2+s3;}public String concatString(String s1,String s2,String s3){StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString();}
在上述代码中,被编译器转换成了StringBuffer类来进行字符串的加操作,而StringBuffer的append方法则是通过synchronized修饰的,由于s1、s2、s3是在栈中操作的,不会被其他线程访问到,这里就没必要使用同步操作,也就会进行锁消除的优化。
锁粗化
锁粗化是指对同步区域的扩大,在上述示例中,三个append方法都是被synchronized修饰,在没有锁竞争的情况下,完全可以将三个方法放在同一个synchronized作用域中,即消除方法的synchronized修饰,改成三个append外加上synchronized修饰。
偏向锁
偏向锁的使用可以看成是无锁操作,但是仅在无线程竞争的情况下。可以理解成,有一个对象锁,当前有一个线程A访问他,此时没有别的线程争用,那么线程A会被锁记住(使用对象头中哈希码的地址空间),线程A获得“偏向锁”,在这段时间内线程A反复使用都不需要加锁,因为线程A被“偏爱”了,只要识别到是线程A,那么永远都是无锁访问。如果这段时间后有新的线程B来使用,此时会首先判断“偏向锁”记住的是不是线程B,如果不是那么通过CAS来修改,修改成功的话B也可以无锁访问,如果修改失败,偏向锁将升级至轻量级锁。
轻量级锁
轻量级锁也是可以看做是无锁操作,同时也是在无线程竞争的情况下存在,区别在于轻量级锁会使用CAS来更新锁的指向,而偏向锁无需CAS。当CAS次数达到指定次数后(JDK5之前通过-XX:PreBlockSpin指定,JDK6之后按照自适应的次数来指定)还不成功会升级至重量级锁。
锁膨胀
锁膨胀其实是针对小部分场景才有效的优化,是从偏向锁到轻量级锁、重量级锁的过程,是为了优化某些情况下(如无线程竞争)使用重量级锁带来的不必要开销。
一张网图概括Synchronized的锁机制

Synchronized与ReentrantLock的性能对比

public class CompareTest {private Lock nonFairLock = new ReentrantLock();private Object monitorLock = new Object();public static void main(String[] args) throws InterruptedException {// test01(1000,1000000);test02(20,100,1000000);// test03();}public static void test01(int cycle,int count) throws InterruptedException {CompareTest test = new CompareTest();long l = System.currentTimeMillis();for (int i = 0; i < cycle; i ++){test.lockAdd(count);}System.out.println("lock 执行时间:"+(System.currentTimeMillis()-l)+" ms");Thread.sleep(1000);long l1 = System.currentTimeMillis();for (int i = 0; i < 1000; i ++){test.syncAdd(count);}System.out.println("synchronized 执行时间:"+(System.currentTimeMillis()-l1)+" ms");}public static void test02(int threadCount,int cycle,int count) throws InterruptedException {CompareTest test = new CompareTest();long l = System.currentTimeMillis();List<Thread> ts = new ArrayList<>();for (int c = 0; c < threadCount; c ++){Thread t = new Thread(() ->{for (int i = 0; i < cycle; i ++){test.lockAdd(count);}});t.start();ts.add(t);}for (Thread t : ts){t.join();}System.out.println("lock 执行时间:"+(System.currentTimeMillis()-l)+" ms");Thread.sleep(1000);long l1 = System.currentTimeMillis();List<Thread> ts1 = new ArrayList<>();for (int c = 0; c < threadCount; c ++){Thread t = new Thread(() ->{for (int i = 0; i < cycle; i ++){test.syncAdd(count);}});t.start();ts1.add(t);}for (Thread t : ts1){t.join();}System.out.println("synchronized 执行时间:"+(System.currentTimeMillis()-l1)+" ms");}public static void test03(int count){}public void lockAdd(int count){nonFairLock.lock();try {int total = 1;for (int i = 0; i < count; i ++){total ++;}}catch (Exception e){e.printStackTrace();}finally {nonFairLock.unlock();}}public void syncAdd(int count){synchronized (monitorLock){int total = 1;for (int i = 0; i < count; i ++){total ++;}}}
Synchronized和Lock该怎么选择
在JDK6以后,synchronizd在性能上有了质的飞跃,甚至在JDK8以后synchronized在一定程度上也表现的比Lock更好,那么synchronized最显著的特点就是简单易用,同时也比较安全。
在一些简单的业务场景或逻辑中,当然是推荐使用synchronized,可以减少lock带来的不必要麻烦。
lock在功能上显然是比synchronized更加丰富,更加灵活,可以实现复杂的同步场景,在需要实现特殊逻辑的情况下lock是更好的选择,当然使用上也会比synchronized更加复杂。
