synchronized锁膨胀、锁升级、锁优化的过程

参考文章
Java中的偏向锁,轻量级锁, 重量级锁解析_萧萧九宸的博客-CSDN博客

本文是本人对以上文章的整理,建议先去看以上文章。

在Java中,一个锁对象的四种状态:

  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁
    在Java中,一个锁就是一个对象

synchronized代码块是由一对monitorentermonitorexit字节码指令实现,这两个指令中间放的就是synchronized中同步代码块中的字节码指令。

在JDK1.6之前,synchronized锁是重量级锁,性能开销严重,从JDK1.6开始,为了改善同步性能,又引入了偏向锁和轻量级锁
锁的重量级的排序是:
无锁 < 偏向锁 < 轻量级锁 < 重量级锁
锁的升级是单向的,只能从低到高,不会出现锁的降级。
随着线程竞争的激烈程度增加,伴随有锁升级的现象,锁的重量程度越来越大。

JVM默认是关闭可偏向锁机制的,如果想要开启可偏向机制,在启动时,通过JVM参数指定

-XX:-UseBiasedLocking

大致的锁膨胀的过程是这样的:

无锁 --> 偏向锁

偏向锁本质上也是一个无锁化编程解决方案。

大部分的情况下,锁不存在多线程竞争,而是总是由同一个线程多次获得,为了减少同一线程获取锁的成本而引入了偏向锁
偏向锁的核心理念就是:如果一个线程获取了锁,那个锁对象就进入偏向模式,“偏向于这个线程”,当这个线程再次请求这个锁时,无需再做同步操作,可以直接获取到该锁,可以省去很多申请锁、加锁的操作,从而减少同步的成本

当初始化锁对象时,会首先判断系统是否开启了“可偏向”机制,如果未开启,则会创建一个普通的无锁状态的对象,并初始化对象头中的内容是hashCode和分代年龄。
若开启了可偏向机制,则会初始化锁对象是可偏向的未偏向状态,即可偏向的无锁状态,此时对象头中MarkWord的内容是 空的线程ID + epoch + 对象分代年龄 + 可偏向标志位

由无锁 到 偏向锁的过程,本质上就是将线程id写入到锁对象的对象头中的MarkWord的过程。
具体的过程是:

  1. 首先读取目标对象的MarkWord,判断此时锁对象是否处于可偏向状态。
  2. 如果是可偏向状态,则尝试用CAS操作,将自己的线程ID写入到锁对象的MarkWord中。
    1. 如果CAS操作成功,则认为此线程已经获取到了对象的偏向锁,执行同步代码块。
    2. 一个线程执行完之后,并不会将锁对象中的MarkWord中的线程ID赋回空值。这样做的好处是:如果线程需要再次对这个锁对象加锁,而之前此锁对象一直没有被其他线程获取过锁,依旧停留在可偏向的状态下,即可以在不修改对象头的情况下,直接获取该对象的偏向锁,直接认为偏向成功
    3. 如果CAS操作失败,则说明有另一个线程B抢先获取了偏向锁。这种状态说明此锁的竞争比较激烈,此时需要撤销线程B的偏向锁,将线程B持有的锁升级为轻量级锁。
  3. 如果是已偏向状态,则检测对象头中的MarkWord中的线程ID是否是当前线程ID
    1. 如果是,则表明本线程已经获取到了偏向锁,可以直接继续执行同步代码块
    2. 如果不相等,则证明该对象偏向于其他线程,需要撤销偏向锁。

注意:偏向锁的撤销过程,并不是将锁对象恢复到无锁可偏向的状态,而是直接将该锁对象的状态修改至“轻量级锁定”状态。

偏向锁 --> 轻量级锁

当存在多个线程竞争某一个对象时,会撤销偏向锁,升级到轻量级锁。
当偏向锁加锁失败后,会升级到轻量级锁。

偏向锁在撤销后,锁对象可能处于两种状态:

  • 一种是不可偏向的无锁状态 (之所以不可偏向,因为系统已经检测到多于一个线程竞争锁对象,升级到了轻量级锁定状态)

  • 另一种是不可偏向的已锁状态 (轻量级锁定)

为什么会出现上述两种状态,因为偏向锁不存在解锁操作,只有撤销操作,触发撤销操作时:

  • 原来已经获取了偏向锁的线程可能已经执行完了同步代码块,使得对象处于“闲置状态”,相当于原有的偏向锁已经过期无效了。此时该对象就应该被直接转换为不可偏向的无锁状态
  • 原来已经获取了偏向锁的线程可能还没有执行完成同步代码块,偏向锁依旧有效,此时锁对象就应该被转换为轻量级锁定的状态

轻量级加锁的过程

  1. 在进入同步代码块的时候,如果锁对象的状态是无锁状态,锁标志位是01。
  2. 会在当前线程的栈帧中创建一个锁记录Lock Record的空间,用来存储锁对象目前的MarkWord的拷贝。(不论是Java方法栈,还是本地方法栈,都是线程私有的
  3. 拷贝对象头中的MarkWord到锁记录中。
  4. 拷贝完成后,JVM会利用CAS操作尝试将锁对象的MarkWord更新为指向锁记录LockRecord的指针,并将Lock Record里的owner指向对象的MarkWord。
  5. 如果更新成功,那么这个线程就拥有了该对象的锁,并且对象的MarkWord的锁标志位置为“00”,表示此对象处于轻量级锁定的状态。

简单点说上面的过程:

  1. 首先检查MarkWord中标志位,锁对象是否处于不可偏向的无锁状态

  2. 然后在当前线程的栈帧中,创建用于存储锁记录LockRecord的空间,并将对象头中的MarkWord拷贝LockRecord中。

  3. 然后尝试用CAS将对象头中的MarkWord替换为指向锁记录的指针。

    • 如果成功,当前线程加锁成功
    • 如果失败,表示该对象已经加锁,先进行自旋操作,再次尝试CAS争抢,如果仍未竞争到,进一步升级到重量级锁。

轻量级锁能够提升程序性能的依据是“绝大部分的情况下,在整个同步期间,是不会存在多线程竞争的”,因此轻量级锁的适应场景是线程交替执行同步代码块的场合,如果同一时间访问同一锁的场合,就会膨胀到重量级锁

重量级锁

重量级锁本质就是操作系统内的管程机制,管程机制就是对信号量操作进行了封装。
关于synchronized重量级锁的原理,可以看我的这篇文章关于Java中synchronized的实现原理_秋天code的博客-CSDN博客

锁优化

自旋锁

轻量级锁加锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,会进行自旋锁的优化手段。
如果要在操作系统层面挂起一个线程,会涉及到用户态切换到和心态,这个过程是需要损耗系统性能的。
自旋锁的假设是,当前线程会在不久后可以获取到锁,因此虚拟机会让该线程进行自旋,经过若干轮等待后,如果得到了锁,就进入临界区。如果还得不到锁,那么在操作系统层面就会真的挂起这个线程。

锁消除

锁消除是虚拟机另外一种锁优化的手段,在JIT编译期间,JVM通过对上下文的扫描,发现有一段临界区是不可能存在竞争的,对于这段临界区来说,没有加锁的必要了,因此会消除这个锁。
例如,一个变量是方法内部的,不可能被其他线程访问到,此时就会消除这个方法的同步。

posted @ 2023-08-14 12:44  秋天Code  阅读(21)  评论(0编辑  收藏  举报  来源