偏向锁撤销发生在另一线程尝试获取同一把锁时,即synchronized进入发现对象头线程ID不匹配便触发,且仅在安全点执行。

偏向锁撤销发生在什么时刻
偏向锁不是永久持有的,只要另一个线程尝试获取同一把锁,JVM 就必须撤销它。这个“尝试获取”不一定要成功——哪怕只是 synchronized 块进入时发现锁对象头里记录的线程 ID 不匹配,就会触发撤销流程。
常见错误现象:BiasedLockingRevoked 日志没开,你以为没撤销,其实每发生一次竞争就悄悄撤了一次;或者压测时 RT 突增,背后是大量偏向锁撤销+批量重偏向的 STW 暂停。
- 撤销只在安全点(safepoint)执行,意味着要等所有线程跑到安全点才能开始,不是“立刻”发生
- 如果锁对象已被多个线程交替竞争过,JVM 会直接禁用该类所有新实例的偏向锁(通过
-XX:BiasedLockingStartupDelay=0也拦不住) - 撤销本身不升级锁,但撤销完成后,下一次锁获取会直接走轻量级锁逻辑(CAS 尝试)
为什么多线程交替竞争会让偏向锁升为轻量级锁
偏向锁的设计前提是“无竞争”或“单线程长期持有”。一旦出现两个及以上线程轮流进入同步块,JVM 认为偏向模式已失效,后续不再尝试重新偏向,而是让锁自然退化到更通用的机制。
关键点在于:撤销 ≠ 升级。撤销只是清除偏向状态;真正“升级”发生在下一次加锁时——此时对象头已无偏向标记,synchronized 会按轻量级锁流程走:先 CAS 设置线程 ID 到锁记录,失败则自旋,再失败才膨胀为重量级锁。
立即学习“Java免费学习笔记(深入)”;
- 交替竞争场景如:线程 A 进入
synchronized(obj)→ 退出 → 线程 B 进入 → 退出 → 线程 A 再进… 第二次 A 进入时,偏向锁早已被撤销,A 的第二次加锁就是轻量级锁的首次 CAS - 注意 JVM 参数
-XX:+UseBiasedLocking默认开启,但 JDK 15+ 已默认关闭,老版本要注意实测行为是否符合预期 - 不要依赖打印对象头判断偏向状态——
java.lang.ClassLayout.parseInstance(obj).toPrintable()显示的只是快照,撤销可能正在排队
如何验证偏向锁是否真的被撤销了
不能只看日志或对象头快照,得抓真实行为。最可靠的方式是观察锁获取路径和 GC safepoint 日志。
实操建议:
- 启动参数加上
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1,看到大量RevokeBias类型 safepoint,说明撤销频繁 - 用 JFR(Java Flight Recorder)录制,过滤事件
jdk.BiasedLockRevocation,能精确定位哪个对象、哪个线程触发了撤销 - 写个最小复现:两个线程轮番执行 100 次
synchronized(obj) { Thread.sleep(1); },然后用jstack查看线程栈里有没有ObjectMonitor相关等待,有就说明已升级为重量级锁;没有且无阻塞,则大概率还在轻量级锁阶段
容易被忽略的性能陷阱
偏向锁撤销本身开销不大,但它的连锁反应很隐蔽:每次撤销都可能引发批量重偏向(bulk rebias),而批量操作需要全局停顿。更麻烦的是,撤销后若紧接着发生自旋竞争,CPU 浪费比直接上轻量级锁还高。
- 对象生命周期短(比如方法内新建的临时对象)还开偏向锁,纯属浪费——撤销成本 > 偏向收益
- 使用
StringBuffer或Vector等内置同步容器时,它们的锁对象极可能被多个线程反复争用,实际运行中偏向锁形同虚设 - JDK 8u60 之后引入了“撤销限制计数器”,同一个类撤销超 20 次就自动禁用该类的偏向锁,但这个阈值不可配,只能绕开(比如换用不同类加载器隔离)
真正要调优的不是“怎么保住偏向锁”,而是确认业务里是否存在稳定单线程持有锁的场景;如果没有,关掉它反而更稳。










