跳转到内容
唯一赫兹
返回

Synchronized 锁机制:从无锁到重量级锁

引言

synchronized 用于将一段代码块标记为同步执行,底层通过将一个对象作为锁,来保证同一时间只有一个线程执行代码块。

对于控制同步代码块的对象锁,它有四种状态:无锁、偏向锁(JDK15+ 弃用)、轻量级锁、重量级锁,锁的状态管理通过「对象头」中的 Mark Word 实现。

一个堆中的对象,主要包含两部分:

Mark Word 是一个 32bit 的数据 1,用于记录对象实例的运行时信息,结构如下表:

锁状态 25bit 4bit 1bit 2bit
23bit2bit是否偏向锁锁标志位
无锁对象的HashCode分代年龄001
偏向锁线程IDEpoch分代年龄101
轻量级锁指向栈中锁记录的指针00
重量级锁指向重量级锁的指针10
GC标记11

无锁就表示没有线程持有这个对象(即作为一个普通的对象),有线程争抢对象后,会根据竞争情况升级到更高级的锁。

锁的实现

锁从设计思想的角度可以分为「乐观锁」和「悲观锁」。

对于一个共享资源,

在具体实现上,可以分为「无锁编程」和「有锁编程

无锁编程性能好,但可能会浪费 CPU(自旋导致 CPU 空转),有锁编程开销高,但不浪费 CPU,于是 synchronized 采用了 锁升级 机制,先自旋几次(轻量级锁),还拿不到锁再阻塞(重量级锁)

偏向锁

偏向锁假定来竞争资源的 总是同一个线程,当线程获得锁后,在 Mark Word 中记下这个线程 id,后续再有线程来的时候,只需要对比 id 是否一致,如果一致则线程直接进入临界区,避免了 CAS(直接比值比 CAS 耗时少)。

但是,偏向锁在 Java15+ 被弃用 2,原因有以下几点:

  1. 现代 CPU 执行 CAS 很快,节省的开销不明显
  2. 对比 id 时,如果不一致,需要撤销偏向锁,然后才能升级为轻量级锁,撤销操作的成本比较高
  3. 符合“一个线程竞争”的场景少

轻量级锁

加锁流程如下:

  1. 在栈帧中创建一个 Lock Record(每次尝试获取锁都会创建一个)
  2. 在 Lock Record 中存储:指向锁对象的指针、锁对象的原始 Mark Word(displaced 字段)
  3. 通过 CAS 尝试将 Mark Word 改为指向 Lock Record 的指针
    1. 成功则进入临界区,失败则检查是否是重入
    2. 是重入则,将 displaced 置为 null(后面解锁会用到), 再进入临界区
    3. CAS 失败并且不是重入则自旋,自旋多次依旧没得到锁就会升级为重量级锁 3

关于重入判断:

判断是否重入的关键在于,Lock Record 位于 线程私有 的栈帧中。因此可以通过 Lock Record 的地址判断其所属线程,从而得知是否是重入。

轻量级锁的加锁流程
轻量级锁的加锁流程
加锁后的内存结构
加锁后的内存结构

解锁流程如下:

  1. 判断是否是重入,是则直接返回
  2. 不是重入则 CAS 恢复 Mark Word
  3. 操作成功则解锁完成,失败则说明已是重量级锁,走重量级锁的解锁流程
轻量级锁的解锁流程
轻量级锁的解锁流程

重量级锁

重量级锁和轻量级锁的关键区别在于,竞争失败后不自旋,而是进入挂起并进入等待队列,解锁时被唤醒再尝试获取锁。

重量级锁的核心是 ObjectMonitor,每个 Java 对象都可以关联一个,重量级锁对象的 Mark Word 就是指向 ObjectMonitor 的指针。

ObjectMonitor 类似于 Lock Record 的升级版本,其大致结构如下:

class ObjectMonitor {
    _header;        // 保存原始的 Mark Word
    _owner;         // 当前持有锁的线程
    _recursions;    // 重入次数
    _EntryList;     // 阻塞等待获取锁的线程队列
    _WaitSet;       // 调用了 wait() 的线程队列
    _cxq;           // 竞争队列(CAS 失败后先进入这里)
    _succ;          // 继任者线程
}
ObjectMonitor职责Lock Record
_header保存原始 Mark Word_displaced_header
_owner标识持锁线程Lock Record 在栈上,栈是线程私有的
_recursions记录重入次数通过多个 Lock Record 栈式结构隐式表示
_EntryList + _cxq等待队列
_WaitSetwait/notify 支持不支持
_succ继任者线程

锁对象头中的 Mark Word 的转换如图:

Footnotes

  1. 32 位 JVM 通常为 32 bit,64 位 JVM 通常为 64 bit。

  2. Java15 默认禁用,可以手动启用;18 时被标记为 deprecated;21 时彻底移除相关代码。

  3. JVM 有自适应自旋策略,自旋次数由 JVM 动态决定而不是固定次数。



下一篇
【JVM 1】JVM 内存分区