Synchronized锁优化原理

JDK5之后,Synchronized实现了各种锁优化策略,如适应性自旋、锁消除、锁粗化、锁膨胀、轻量级锁、偏向锁等。

适应性自旋

CAS自旋是通过硬件实现的计算机原语来实现的

通常情况下的CAS是比较并替换,在无限循环中直至替换成功

  1. for(;;){
  2. //compareAndSet是本地方法,底层是通过硬件实现的一组计算机原语
  3. if(compareAndSet(expect,news)){
  4. return news;
  5. }
  6. }

在早期的(JDK5及以前)JVM中,自旋锁默认是关闭的,可以通过-XX:+UseSpinning参数开启,并使用-XX:PreBlockSpin参数来自定义自旋次数,默认是10次,这些参数是全局设定的,对于不同的锁都是一样的效果。类似于下面代码

  1. for(int i = 0; i < 10; i ++){
  2. if(compareAndSet(expect,news)){
  3. return news;
  4. }
  5. }

在JDK6之后,对自旋锁进行了优化,也就是优化成了适应性自旋锁,他的自旋次数是由上一次锁的自旋时间及锁的拥有者(持锁线程)的状态决定,如果自旋获取锁的成功率更高,那么自旋允许的次数也相对较多,可能是几十或者上百次。
如果自旋获取锁的成功率较低,说明竞争较大,同步区的执行时间较长,那么自旋次数也相对较少,改用阻塞返回更有利于性能的提升。

锁消除

锁消除顾名思义就是不使用锁,取消锁。这种情况是为了优化一些隐藏式的同步过程,一些看似没有使用锁的代码,也有可能会被编译成同步代码,例如JDK5之前,字符串的相加是会被转换成StringBuffer类来操作的

  1. public String concatString(String s1,String s2,String s3){
  2. return s1+s2+s3;
  3. }
  4. public String concatString(String s1,String s2,String s3){
  5. StringBuffer sb = new StringBuffer();
  6. sb.append(s1);
  7. sb.append(s2);
  8. sb.append(s3);
  9. return sb.toString();
  10. }

在上述代码中,被编译器转换成了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的锁机制

20190323140321501.png

Synchronized与ReentrantLock的性能对比

image.png

  1. public class CompareTest {
  2. private Lock nonFairLock = new ReentrantLock();
  3. private Object monitorLock = new Object();
  4. public static void main(String[] args) throws InterruptedException {
  5. // test01(1000,1000000);
  6. test02(20,100,1000000);
  7. // test03();
  8. }
  9. public static void test01(int cycle,int count) throws InterruptedException {
  10. CompareTest test = new CompareTest();
  11. long l = System.currentTimeMillis();
  12. for (int i = 0; i < cycle; i ++){
  13. test.lockAdd(count);
  14. }
  15. System.out.println("lock 执行时间:"+(System.currentTimeMillis()-l)+" ms");
  16. Thread.sleep(1000);
  17. long l1 = System.currentTimeMillis();
  18. for (int i = 0; i < 1000; i ++){
  19. test.syncAdd(count);
  20. }
  21. System.out.println("synchronized 执行时间:"+(System.currentTimeMillis()-l1)+" ms");
  22. }
  23. public static void test02(int threadCount,int cycle,int count) throws InterruptedException {
  24. CompareTest test = new CompareTest();
  25. long l = System.currentTimeMillis();
  26. List<Thread> ts = new ArrayList<>();
  27. for (int c = 0; c < threadCount; c ++){
  28. Thread t = new Thread(() ->{
  29. for (int i = 0; i < cycle; i ++){
  30. test.lockAdd(count);
  31. }
  32. });
  33. t.start();
  34. ts.add(t);
  35. }
  36. for (Thread t : ts){
  37. t.join();
  38. }
  39. System.out.println("lock 执行时间:"+(System.currentTimeMillis()-l)+" ms");
  40. Thread.sleep(1000);
  41. long l1 = System.currentTimeMillis();
  42. List<Thread> ts1 = new ArrayList<>();
  43. for (int c = 0; c < threadCount; c ++){
  44. Thread t = new Thread(() ->{
  45. for (int i = 0; i < cycle; i ++){
  46. test.syncAdd(count);
  47. }
  48. });
  49. t.start();
  50. ts1.add(t);
  51. }
  52. for (Thread t : ts1){
  53. t.join();
  54. }
  55. System.out.println("synchronized 执行时间:"+(System.currentTimeMillis()-l1)+" ms");
  56. }
  57. public static void test03(int count){
  58. }
  59. public void lockAdd(int count){
  60. nonFairLock.lock();
  61. try {
  62. int total = 1;
  63. for (int i = 0; i < count; i ++){
  64. total ++;
  65. }
  66. }catch (Exception e){
  67. e.printStackTrace();
  68. }finally {
  69. nonFairLock.unlock();
  70. }
  71. }
  72. public void syncAdd(int count){
  73. synchronized (monitorLock){
  74. int total = 1;
  75. for (int i = 0; i < count; i ++){
  76. total ++;
  77. }
  78. }
  79. }

Synchronized和Lock该怎么选择

在JDK6以后,synchronizd在性能上有了质的飞跃,甚至在JDK8以后synchronized在一定程度上也表现的比Lock更好,那么synchronized最显著的特点就是简单易用,同时也比较安全。
在一些简单的业务场景或逻辑中,当然是推荐使用synchronized,可以减少lock带来的不必要麻烦。
lock在功能上显然是比synchronized更加丰富,更加灵活,可以实现复杂的同步场景,在需要实现特殊逻辑的情况下lock是更好的选择,当然使用上也会比synchronized更加复杂。