Java synchronized 锁升级过程详解
Java中的synchronized
锁从JDK 1.6开始实现了锁升级机制,这是JVM为了在不同竞争场景下都能获得最优性能而设计的精巧优化。锁升级过程遵循"偏向锁→轻量级锁→重量级锁"的路径,下面我将详细解析每个阶段的转换过程。
一、对象头结构基础
在了解锁升级前,需要先理解Java对象头的结构(以64位JVM为例):
|---------------------------------------------------------------------|
| Mark Word (64 bits) |
|---------------------------------------------------------------------|
| 锁状态 | 存储内容 |
|---------------------------------------------------------------------|
| 无锁 | unused:25 | identity_hashcode:31 | unused:1 | age:4 | 0|01 |
| 偏向锁 | thread:54 | epoch:2 | unused:1 | age:4 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针:62 | 00 |
| 重量级锁 | 指向互斥量(mutex)的指针:62 | 10 |
| GC标记 | 空(不需要记录信息) | 11 |
二、锁升级完整流程
1. 初始状态 - 无锁 (001)
当对象刚被创建时:
- 锁标志位是
001
- 没有线程持有该对象锁
- 可以计算identity hashcode(一旦计算会禁用偏向锁)
2. 偏向锁阶段 (101)
触发条件:
- 第一个线程访问同步块
- JVM启用了偏向锁(默认开启,但有4秒延迟)
升级过程:
- 检查对象头中的锁标志位是否为
001
(可偏向状态) - 通过CAS操作尝试将Mark Word中的Thread ID替换为当前线程ID:
- 成功:进入偏向模式,锁标志位变为
101
- 失败:说明存在竞争,开始撤销偏向锁
- 成功:进入偏向模式,锁标志位变为
偏向锁数据结构:
|---------------------------------------------------------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | 1 | 01 |
|---------------------------------------------------------------------|
特点:
- 后续该线程进入同步块只需检查Thread ID是否匹配
- 几乎不产生同步开销
- 适合单线程重复访问的场景
撤销偏向锁:
当其他线程尝试获取已被偏向的锁时:
- 暂停持有偏向锁的线程(到达安全点)
- 检查原持有线程是否仍处于同步块中:
- 已退出:恢复到无锁状态(
001
) - 仍活跃:升级为轻量级锁(
00
)
- 已退出:恢复到无锁状态(
3. 轻量级锁阶段 (00)
触发条件:
- 多个线程交替访问同步块(无真正竞争)
- 偏向锁被撤销
升级过程:
- 在当前线程栈帧中创建锁记录(Lock Record)
- 将对象头Mark Word复制到锁记录中(Displaced Mark Word)
- 通过CAS尝试将对象头替换为指向锁记录的指针:
- 成功:获取轻量级锁
- 失败:存在竞争,开始自旋或升级
轻量级锁数据结构:
对象头:
|---------------------------------------------------------------------|
| 指向栈中锁记录的指针:62 | 00 |
|---------------------------------------------------------------------|
栈帧中的锁记录:
|---------------------------------------------------------------------|
| Displaced Mark Word | 对象指针 |
|---------------------------------------------------------------------|
自旋优化:
- 失败线程不会立即阻塞,而是自旋尝试获取锁
- 自旋次数由JVM自适应调整(基于上次获取情况)
- 避免线程切换的开销
升级到重量级锁:
当:
- 自旋超过阈值(默认10次)
- 或等待线程数超过CPU核数的一半
4. 重量级锁阶段 (10)
实现机制:
- 对象头指向操作系统级别的互斥量(mutex)
- 未获取锁的线程进入阻塞状态
- 依赖操作系统的线程调度
重量级锁数据结构:
|---------------------------------------------------------------------|
| 指向互斥量(mutex)的指针:62 | 10 |
|---------------------------------------------------------------------|
特点:
- 上下文切换开销大
- 适合高竞争场景
- 线程阻塞会触发操作系统调度
三、锁升级状态转换图
四、关键优化细节
1. 批量重偏向(Bulk Rebias)
- 问题:一类对象被多个线程交替访问(如生产者-消费者模式)
- 解决:当一类对象的偏向锁撤销超过阈值(默认20次)
- 操作:JVM会批量重偏向这些对象到新线程
2. 批量撤销(Bulk Revoke)
- 问题:一类对象明显存在多线程竞争
- 解决:当一类对象的偏向锁撤销超过另一阈值(默认40次)
- 操作:JVM会禁用该类的偏向锁功能
3. 偏向锁延迟启用
- JVM启动后4秒(可通过
-XX:BiasedLockingStartupDelay=0
禁用) - 避免启动阶段大量类加载导致的偏向锁撤销
4. 锁降级
- 重量级锁不会降级为轻量级锁
- 轻量级锁释放后直接回到无锁状态
五、性能影响对比
场景 | 偏向锁 | 轻量级锁 | 重量级锁 |
---|---|---|---|
单线程访问 | 纳秒级 | 需要CAS | 系统调用 |
低竞争交替访问 | 需撤销 | 微秒级 | 系统调用 |
高竞争 | 频繁撤销 | 自旋消耗CPU | 线程阻塞 |
内存占用 | 无额外开销 | 栈帧锁记录 | 重量级元数据 |
六、最佳实践
- 减少同步范围:只在必要代码块加锁
- 降低锁粒度:拆分大锁为多个小锁
- 避免长时间同步:减少持有锁的时间
- 监控锁竞争:使用JConsole或VisualVM观察锁状态
- 高竞争场景:考虑使用
java.util.concurrent
中的显式锁
锁升级机制使得synchronized
在无竞争和低竞争场景下性能接近无锁,同时在高竞争时仍能保证正确性,是Java并发性能优化的重要成果。