偏向锁撤销触发STW(Stop-The-World)的根本原因在于其撤销操作需要全局内存一致性和线程状态确定性,具体机制如下:
⚙️ 一、偏向锁撤销的核心流程
- 竞争触发撤销
当线程B尝试获取已被线程A偏向的锁时,JVM检测到竞争,触发撤销操作。 - 挂起持有线程
需暂停持有偏向锁的线程A,检查其是否仍在同步块中:- 若线程A已退出同步块:直接撤销偏向锁,恢复为无锁状态(标志位
01
)。 - 若线程A仍在同步块中:升级为轻量级锁(标志位
00
),线程B继续竞争。
- 若线程A已退出同步块:直接撤销偏向锁,恢复为无锁状态(标志位
- 对象头修改
将对象头(Mark Word)中的线程ID移除,替换为轻量级锁指针或无锁状态。
🛑 二、STW的必要性
撤销操作必须在全局安全点(Safepoint) 进行,原因如下:
- 线程状态一致性
需确保所有线程(尤其是持有偏向锁的线程)处于可安全挂起的状态(如未执行字节码),防止撤销过程中线程修改对象头或栈帧。 - 线程栈遍历需求
判断线程A是否在同步块中,需遍历其栈帧中的Lock Record
(锁记录)。若线程未暂停,栈帧可能动态变化,导致数据不一致。 - 内存屏障要求
对象头修改需原子性,且其他线程必须立即可见。STW期间可避免CPU缓存与主存不一致问题。
⏱️ 三、STW的性能影响
- 短暂停顿:单次撤销通常耗时微秒级(如0.1-1ms),对低竞争场景影响小。
- 高并发瓶颈:频繁竞争会导致多次撤销,累积STW时间显著增加(如百线程竞争时可达毫秒级)。
- 日志示例:
启用安全点日志(-XX:+PrintSafepointStatistics
)可见:
其中RevokeBias [threads: 200 initially_running: 5] [time: spin=0.1ms block=2ms cleanup=0.5ms vmop=15ms]
vmop=15ms
为实际STW时间。
⚖️ 四、不同场景下的撤销处理
场景 | 处理方式 | 是否需STW |
---|---|---|
持有线程已退出同步块 | 直接恢复为无锁状态 | 是(但操作更快) |
持有线程仍在同步块中 | 升级为轻量级锁 | 是 |
调用hashCode() /wait() | 强制撤销并升级重量级锁 | 是 |
调用
hashCode()
会覆盖Mark Word中的线程ID,强制撤销偏向锁。
🚀 五、优化建议
- 禁用偏向锁
高并发场景(如秒杀系统)通过-XX:-UseBiasedLocking
禁用,避免频繁撤销导致的STW。 - 监控安全点
临时开启日志定位非GC导致的STW:-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1
- 升级JDK版本
JDK 15+ 默认关闭偏向锁,JDK 18+ 已移除该机制,彻底规避此问题。
💎 总结
偏向锁撤销触发STW的本质是保证内存与线程状态一致性的代价。在单线程场景中,偏向锁通过避免CAS操作提升性能;但在多线程竞争下,其撤销机制反而成为瓶颈。高并发系统应禁用偏向锁,选择轻量级锁(CAS)或直接使用java.util.concurrent
中的并发工具(如ReentrantLock
),以消除STW对延迟的影响。