在Java并发编程中,synchronized 是最基础的同步手段。然而,早期JVM中 synchronized 直接基于操作系统的互斥量(重量级锁)实现,每次加锁/解锁都会引发用户态与内核态的切换,性能开销较大。为了优化这一场景,HotSpot 虚拟机在 Java 6 中引入了锁升级 机制,让锁可以根据竞争情况动态变化。这一机制使得 synchronized 的性能在无竞争或低竞争场景下大幅提升。
本文将带你深入理解锁的状态演变,从对象头中的 Mark Word 说起,一步步剖析偏向锁、轻量级锁、重量级锁的升级过程,以及背后的设计思想。
一、锁状态的存储:对象头与Mark Word
每个Java对象在内存中都包含一个对象头 (Object Header)。对象头中有一个关键字段 ------ Mark Word ,它存储了对象的运行时数据,包括哈希码、GC分代年龄以及锁状态信息。锁状态的记录正是通过 Mark Word 的不同位模式来实现的。
以32位JVM为例,Mark Word的典型布局如下:
| 锁状态 | 25位 | 4位 | 1位(偏向) | 2位(锁标志) |
|---|---|---|---|---|
| 无锁 | 对象哈希码 | 分代年龄 | 0 | 01 |
| 偏向锁 | 线程ID | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
| 重量级锁 | 指向重量级锁的指针 | 10 |
锁标志位(最后2位)和偏向位(倒数第3位)共同决定了当前锁的状态。
二、四种锁状态
1. 无锁
对象初始状态,没有线程持有锁。Mark Word中存储对象的哈希码和分代年龄。
2. 偏向锁(Biased Locking)
设计初衷:在实际应用中,很多锁始终由同一个线程获取,例如单线程环境或某些工具类(如Vector、Hashtable)内部使用。偏向锁假设锁总是由同一个线程持有,因此当该线程再次进入同步块时,只需检查对象头中的线程ID是否匹配,无需任何CAS操作。
特点:
-
当线程第一次获取锁时,JVM通过CAS将线程ID写入Mark Word,并将偏向位置1。
-
后续该线程再次进入同步块,直接比较线程ID,成功则进入,无需任何同步开销。
-
偏向锁不会主动释放,只有遇到竞争时才撤销。
3. 轻量级锁(Lightweight Locking)
设计初衷 :当出现少量竞争时,JVM希望避免重量级锁带来的阻塞开销,转而使用自旋(CAS)尝试获取锁,如果自旋成功则无需进入阻塞。
特点:
-
当另一个线程尝试获取已偏向的锁时,偏向锁撤销并升级为轻量级锁。
-
线程会在栈帧中创建锁记录(Lock Record),并通过CAS将对象头的Mark Word替换为指向锁记录的指针。
-
如果CAS成功,该线程获得锁;如果失败,说明有竞争,轻量级锁会尝试自旋等待。
-
轻量级锁适合线程交替执行、竞争不激烈的场景。
4. 重量级锁(Heavyweight Locking)
当竞争激烈,轻量级锁的自旋超过一定次数(JDK 1.6以后自适应自旋)仍然无法获得锁时,锁会膨胀为重量级锁。此时,线程会进入阻塞状态,由操作系统内核调度。
特点:
-
重量级锁的Mark Word指向一个监视器对象(Monitor),关联操作系统的互斥量。
-
未获得锁的线程会阻塞,直到锁被释放。
-
适用于竞争激烈、锁持有时间较长的场景。
三、锁的升级过程(膨胀路径)
锁的升级是单向的,只能从低级别向高级别升级,不能降级。
步骤1:无锁 → 偏向锁
-
当线程第一次进入同步块时,JVM检查Mark Word是否为无锁状态(标志位01,偏向位0)。
-
通过CAS将当前线程ID写入Mark Word,并设置偏向位为1。
-
若CAS成功,对象进入偏向锁状态;若失败(说明其他线程已经偏向),则触发偏向锁撤销。
步骤2:偏向锁 → 轻量级锁
-
当另一个线程尝试获取该锁时,发现Mark Word中的线程ID不是自己,此时需要撤销偏向锁。
-
撤销操作需要在全局安全点(SafePoint)执行,暂停持有偏向锁的线程,检查该线程是否存活以及是否退出同步块。
-
如果原线程已退出,则将对象头恢复为无锁状态,然后重新竞争;如果原线程仍存活,则升级为轻量级锁,让原线程持有锁,新线程自旋尝试获取。
步骤3:轻量级锁 → 重量级锁
-
当锁处于轻量级锁状态,且有线程自旋等待超过一定次数(或自适应自旋认为不适合自旋)时,锁会膨胀为重量级锁。
-
此时,JVM会为对象分配一个Monitor对象,将Mark Word指向该Monitor,并将所有等待线程阻塞。
-
重量级锁下,加锁和解锁都通过操作系统的互斥量完成。
四、偏向锁的撤销与批量重偏向
偏向锁的撤销成本较高(需要在安全点暂停线程),因此JVM引入了一些优化:
-
批量重偏向:当某个类的大量对象发生偏向锁撤销时,JVM会认为该类可能不再适合偏向锁,于是将相关对象的偏向锁批量撤销,并重新偏向到新线程。
-
批量撤销:如果撤销次数超过阈值,JVM会禁止该类的偏向锁功能,所有后续对象都直接进入轻量级锁。
这些优化避免了频繁撤销带来的性能损耗。
五、锁升级的图示
无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁
↑ ↓ ↓
└─────────┴───────────┘(单向升级)
六、锁状态的选择与性能考量
-
无锁:初始状态。
-
偏向锁:适用于单线程反复获取同一锁的场景,能消除所有同步开销。
-
轻量级锁:适用于多线程交替执行、竞争不激烈的场景,通过CAS自旋避免阻塞。
-
重量级锁:适用于竞争激烈、锁持有时间长的场景,阻塞等待,避免CPU空转。
注意:在JDK 15中,偏向锁被默认禁用(JEP 374),并在后续版本中逐步移除。这是因为现代应用多为高并发,偏向锁的撤销成本反而成为性能负担。因此,在大多数情况下,锁升级路径会直接从无锁进入轻量级锁,再升级到重量级锁。
七、总结
Java锁的状态演变是JVM为了平衡性能与正确性而设计的精巧机制。通过对象头中的Mark Word记录锁状态,并动态升级,使得 synchronized 在无竞争时几乎零开销,在有竞争时也能保证线程安全。
理解锁升级过程,有助于我们更好地诊断并发问题,例如:
-
为什么
synchronized在高并发下性能下降?(升级为重量级锁,线程阻塞) -
为什么偏向锁在某些场景下反而拖慢性能?(频繁撤销导致安全点暂停)
随着JDK的发展,锁优化也在不断演进。尽管偏向锁逐渐淡出,但轻量级锁和自适应自旋依然是现代JVM中 synchronized 性能的重要保障。作为开发者,我们应当根据业务场景选择合适的并发控制手段,在保证正确性的同时,追求更优的性能表现。