1. 锁升级机制概述
在Java并发编程中,synchronized关键字是最常用的同步机制之一。早期的synchronized实现效率较低,被称为"重量级锁",因为它直接使用操作系统层面的互斥锁(mutex)来实现同步。随着JVM的发展,JDK 1.6引入了锁升级机制,使得synchronized能够根据实际竞争情况动态调整锁的实现方式,从而大幅提升性能。
锁升级的核心思想是:根据线程竞争的激烈程度,从偏向锁→轻量级锁→重量级锁逐步升级。这种自适应策略能够在不同场景下提供最优的性能表现:
- 无竞争场景:偏向锁几乎无开销
- 低竞争场景:轻量级锁通过CAS自旋减少上下文切换
- 高竞争场景:重量级锁通过阻塞避免CPU空转
2. Java对象头与锁状态
2.1 Mark Word结构
锁的状态信息存储在Java对象头的Mark Word中。在64位JVM中,Mark Word的结构如下:
| 锁状态 | 存储内容 | 标识位 | 偏向位 |
|---|---|---|---|
| 无锁 | 哈希码(31)+分代年龄(4)+未使用(25) | 01 | 0 |
| 偏向锁 | 线程ID(54)+Epoch(2)+分代年龄(4) | 01 | 1 |
| 轻量级锁 | 指向栈帧锁记录的指针(62) | 00 | - |
| 重量级锁 | 指向互斥锁(mutex)的指针(62) | 10 | - |
2.2 锁状态转换
锁状态的转换是通过修改Mark Word中的字段实现的:
- 无锁→偏向锁:设置偏向位为1,写入线程ID
- 偏向锁→轻量级锁:撤销偏向状态,创建锁记录
- 轻量级锁→重量级锁:替换为指向mutex的指针
3. 偏向锁实现细节
3.1 偏向锁工作原理
偏向锁的设计初衷是优化无竞争场景下的同步性能。其核心特点是"偏向"第一个获取它的线程:
- 线程首次获取锁时,JVM通过CAS将Mark Word的偏向位设为1,并记录线程ID
- 该线程再次进入同步块时,只需检查线程ID是否匹配
- 匹配则直接进入,无需任何同步操作
- 不匹配则触发偏向锁撤销
注意:偏向锁在退出同步块时不会主动释放,Mark Word中的偏向信息会保留,以便下次快速获取。
3.2 偏向锁撤销流程
当出现竞争时,偏向锁需要撤销并升级为轻量级锁。撤销过程需要暂停持有锁的线程(安全点):
- 检查持有线程是否仍处于同步块中
- 如果已退出,恢复为无锁状态
- 如果仍在同步块中,升级为轻量级锁
- 唤醒暂停的线程继续执行
3.3 偏向锁触发条件
偏向锁会在以下情况下被撤销并升级:
- 其他线程尝试获取锁(最常见情况)
- 调用对象的hashCode()方法(因为Mark Word需要存储哈希码)
- 批量撤销(同一类的多个对象频繁发生竞争)
4. 轻量级锁实现机制
4.1 轻量级锁工作流程
轻量级锁适用于线程交替执行的场景,通过CAS自旋避免线程阻塞:
- 在栈帧中创建锁记录(Lock Record)
- 将对象头的Mark Word复制到锁记录中(Displaced Mark Word)
- 通过CAS将对象头替换为指向锁记录的指针
- 成功则获取锁,失败则自旋重试
4.2 轻量级锁释放过程
轻量级锁的释放同样通过CAS完成:
- 将Displaced Mark Word写回对象头
- 如果成功,锁被释放
- 如果失败,说明已升级为重量级锁
4.3 自旋优化策略
轻量级锁的自旋不是无限制的,JDK采用了适应性自旋策略:
- 默认最大自旋次数为10次(-XX:PreBlockSpin)
- 根据历史成功率动态调整自旋次数
- 如果自旋失败率较高,会减少自旋次数
5. 重量级锁实现原理
5.1 重量级锁核心机制
当竞争激烈时,轻量级锁会升级为重量级锁:
- JVM向操作系统申请互斥锁(mutex)
- 将对象头指向该mutex
- 未获取锁的线程进入阻塞状态
- 锁释放时唤醒等待线程
5.2 重量级锁性能特点
重量级锁的主要开销来自:
- 系统调用(内核态/用户态切换)
- 线程上下文切换
- 线程阻塞/唤醒操作
测试表明,重量级锁的开销比轻量级锁高10倍以上。
6. 锁升级不可逆性分析
6.1 设计角度考量
锁升级不可逆的核心原因:
- 降级带来的性能提升有限
- 状态切换本身有显著开销
- 高竞争场景下,降级可能导致性能回退
6.2 实现成本分析
具体实现上的限制:
- 重量级锁降级需要操作系统参与
- 线程状态难以逆向恢复
- 检测竞争强度需要额外开销
6.3 实际场景验证
生产环境观察表明:
- 锁升级通常反映真实的竞争模式
- 降级后很可能很快又需要升级
- 频繁升降级会导致性能抖动
7. 锁优化实践建议
7.1 参数调优
可以通过JVM参数调整锁行为:
- -XX:+UseBiasedLocking:启用偏向锁(默认开启)
- -XX:BiasedLockingStartupDelay=0:立即启用偏向锁
- -XX:PreBlockSpin=10:设置最大自旋次数
7.2 编码最佳实践
- 减少同步块大小
- 避免在同步块中调用hashCode()
- 对于高竞争场景考虑使用Lock替代
8. 常见问题排查
8.1 性能问题定位
锁竞争导致的性能问题可以通过以下工具诊断:
- Jstack查看线程状态
- JFR(Java Flight Recorder)分析锁竞争
- JMC(Java Mission Control)可视化监控
8.2 典型错误认知
需要纠正的常见误区:
- 认为synchronized总是重量级锁
- 忽视锁升级的开销
- 过度依赖自旋优化
9. 实际案例分析
9.1 偏向锁失效场景
案例:调用hashCode()导致偏向锁撤销
java复制Object obj = new Object();
synchronized(obj) {
// 偏向锁生效
obj.hashCode(); // 触发偏向锁撤销
synchronized(obj) {
// 此时已经是轻量级锁
}
}
9.2 轻量级锁自旋优化
案例:短时锁竞争下的性能表现
java复制// 线程1
synchronized(lock) {
// 短时间操作
}
// 线程2(稍后启动)
synchronized(lock) {
// 轻量级锁自旋成功
}
10. 未来发展趋势
随着硬件发展,锁机制也在持续优化:
- 偏向锁在NUMA架构下的改进
- 自旋策略的智能化调整
- 与协程等新特性的结合
不过锁升级的基本原理仍然适用,理解这些底层机制对于编写高性能并发程序至关重要。