Java并发——锁的状态演变

在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 性能的重要保障。作为开发者,我们应当根据业务场景选择合适的并发控制手段,在保证正确性的同时,追求更优的性能表现。

相关推荐
tt5555555555553 分钟前
电工证学习笔记(二)
笔记·学习·电力
破z晓5 分钟前
javascript 导出excel表
开发语言·javascript·excel
西门啐血31 分钟前
上位机开发之假装有设备,使用 C# 模拟串口设备
开发语言·mongodb·c#
QQ_216962909638 分钟前
Spring Boot 养老院管理系统:从入住、护理到费用结算的全流程实现(源码可领)
java·spring boot·后端
cxoptics1 小时前
冰洲石分束器设计与应用
java
ck-joker1 小时前
M1 Pro跑LLM实测:Ollama+LangChain4j零成本本地大模型开发,比云端API快在哪?
java·语言模型
阿米亚波2 小时前
【C++ STL】std::unordered_multiset
开发语言·数据结构·c++·笔记·stl
丙氨酸長鏈2 小时前
[Bukkit插件开发]手持发射器箭矢机枪 教学文档 面向Python/C#开发者入门Java与Bukkit API
java·python·c#
Nontee2 小时前
设计模式:模板方法与策略,从“每个字都认识“到能说清它们在干嘛
java·数据库·设计模式
我是唐青枫2 小时前
Java ReentrantLock 实战详解:比 synchronized 更灵活的可重入锁
java·开发语言