> 文章列表 > JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

文章目录

  • 1、锁指向总结
  • 2、锁升级过程
    • 2.1、无锁
    • 2.2、偏向锁
      • 2.2.1、什么时候升级成偏向锁?
      • 2.2.1、偏向锁的原理?
      • 2.2.3、偏向锁开启条件?
    • 2.3、撤销偏向锁
      • 2.3.1、发生的条件
      • 2.3.2、全局安全点的概念
    • 2.4、轻量锁(CAS)
      • 2.4.1、发生的时机
      • 2.4.2、加锁
      • 2.4.3、解锁
      • 2.4.4、释放CAS锁
    • 2.5、重量级锁/锁膨胀
      • 2.5.1、发生的时机
      • 2.5.2、步骤原理
  • 3、锁升级后,hashCode去哪里了
  • 4、各种锁的优缺点对比
  • 5、JIT编译器对锁的优化
    • 5.1、锁消除
    • 5.2、锁粗化

前言:本章知识点比较繁杂,面向的是有一定JUC基础的小伙伴,纯小白可能看起来不费劲。因为知识点较多所以我自己个人画出了思维导图帮助理解,全文也基本上是按照是思维导图来的,我也是最近刚复习JUC,有任何我说错的地方,欢迎大家指出,互相学习

1、锁指向总结

先说好结论 ,全文都要都是围绕这几点进行分析的,看不得懂得可以看后面的分析讲解

锁对象 锁指向
偏向锁 MarkWord存储的是偏向线程的ID
轻量锁 MarkWord存储的是线程栈中的LockRecord指针
重量锁 MarkWord存储是指向堆中的monitor对象的指针

2、锁升级过程

下图是锁升级过程中的标记位,展示图,全文都是围绕这些进行讲解
JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

2.1、无锁

没啥好讲的, 没啥竞争, 记好他们的标记位即可。

  • 偏向锁位 0 锁标志位: 01 ;

2.2、偏向锁

这里是整个流程的总结;
JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

2.2.1、什么时候升级成偏向锁?

  • 当高并发的条件下,可能获取锁的都是同一个线程;
  • 当一段同步代码一直被同一个线程多次访问后,由于只有一个线程那么该线程在后续的访问会直接获取锁,懒得连cas都不操作了,直接提高了程序的性能

2.2.1、偏向锁的原理?

当同一个线程反复抢到锁,这个线程我们称之为偏向线程, 我们只需要在他第一次获取锁的时候,记录下偏向线程的ID,这样偏向线程就一直持有锁(后序这个线程进入和退出这段加了同步锁的代码块,不需要再次的加锁和释放锁.而是直接去检查锁的MarkWord里面放的是不是自己的线程ID)

如果相等

  • 如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁.以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步.无需每次加锁解锁都去更新对象头.如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高.

如果不等

  • 如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID,
    – 竞争成功: 表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
    – 这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。

2.2.3、偏向锁开启条件?

  • jdk1.8 默认开启,但是有4s的延迟, 可以通过参数, 把延迟设置为0
  • -XX:BiasedLockingStartupDelay=0
  • 特殊说明 : java15废弃偏向锁,默认关闭[开销和成本大]

2.3、撤销偏向锁

JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

2.3.1、发生的条件

如果一个线程持有 synchronized 块的偏向锁,而另一个线程也想要获取该锁,那么偏向锁就会失效,发生锁的撤销和竞争

2.3.2、全局安全点的概念

偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

  • 当第二个线程尝试获取偏向锁时,偏向锁会失效,JVM 会检查当前持有偏向锁的线程是否仍在执行同步代码块。
  • 如果持有偏向锁的线程仍在执行同步代码块,那么JVM 会暂停第二个线程,等待第一个线程执行完同步代码块后释放锁。
  • 如果持有偏向锁的线程已经执行完同步代码块并释放了锁,那么JVM 会将锁升级为轻量级锁,然后继续执行第二个线程。
  • 如果第二个线程竞争锁失败,那么锁就会继续保持在轻量级锁状态,等待下一次竞争。

2.4、轻量锁(CAS)

我知道这里分析的逻辑比较乱,给你们准备好了思维导图
JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

2.4.1、发生的时机

  • 多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。

2.4.2、加锁

如果CAS交换成功

  • 每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
    JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)
  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
    JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)
  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
    JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)
    如果交换失败
  • 如果是重入锁
    – 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数[重入也会进入cas交换,但是此时他会失败,失败也没关系,他知道是他自己加,此时他存入的不是markWord啦, 是null ,表示我重入了几次, 就有几个null]
  • 不是重入锁
    – 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

特点

  1. 在 Java 6 之后自旋锁是自适应的;
  2. 线程如果自旋成功了,那么下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也会很大概率成功
  3. 如果很少自旋成功,那么下次自旋的次数会减少,甚至不自旋,避免cpu空转
  4. Java 7 之后不能控制是否开启自旋功能

2.4.3、解锁

如果是重入锁

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
    JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)
    不是重入锁
  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

2.4.4、释放CAS锁

在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

2.5、重量级锁/锁膨胀

2.5.1、发生的时机

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

2.5.2、步骤原理

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
    JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)
    这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址然后自己进入 Monitor 的 EntryList BLOCKED
    JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 [因为object已经指向了Monitor锁]

3、锁升级后,hashCode去哪里了

JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

4、各种锁的优缺点对比

  • 偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
  • 轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方-法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
  • 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。

5、JIT编译器对锁的优化

JIT Just in time compiler 及时编译器, Java中会对我们写的代码,进行一些合理的优化;

5.1、锁消除

从JIT的角度相当于无视了synchronized(o) 不存在了;这个锁并没有共用扩散到其他线程使用
JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)

5.2、锁粗化

假如方法中首尾相接,前后相邻的都是同一个锁对象,那了IT编译器就会把这几个synchronized块合并成一个大块. 加粗加大范围,一次申请锁使用即可,避免次次的申请和隆放锁,提升了性能
JUC并发编程高级篇第六章之Synchronized锁升级(无锁->偏向锁->自旋锁->重量锁)