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

相关推荐
鄭郑2 小时前
Figma学习笔记--02
笔记·学习·figma
2501_945424802 小时前
C++与硬件交互编程
开发语言·c++·算法
2301_818419012 小时前
C++中的表达式模板
开发语言·c++·算法
Roselind_Yi2 小时前
排查Visual C++堆损坏(HEAP CORRUPTION)错误:从报错到解决的完整复盘
java·开发语言·c++·spring·bug·学习方法·远程工作
ZoeJoy82 小时前
C# Windows Forms 学生成绩管理器(StudentGradeManager)—— 方法重载、out、ref、params 参数示例
开发语言·c#
bing_1582 小时前
spring Boot 3.0 和2.0的区别
java·spring boot·后端
Thomas.Sir2 小时前
Shiro认证与授权:Java安全框架的核心机制
java·安全·shiro·权限控制
千百元2 小时前
网络图标显示不正常
开发语言·网络·php
Amumu121382 小时前
Js: ES新特性(一)
开发语言·前端·javascript