锁消除是JIT通过逃逸分析确认对象未逃逸后,直接删除monitorenter/monitorexit字节码的优化;需满足局部新对象、无逃逸、C2编译触发等条件,否则不会生效。

锁消除不是你写了 synchronized 就能被删的,而是 JIT 在逃逸分析确认“这把锁根本没人争”之后,直接抹掉 monitorenter/monitorexit 字节码——连锁指令都不生成。
为什么本地跑几行测试代码永远看不到锁消除
逃逸分析和锁消除都发生在 C2 编译阶段,不是解释执行时发生的。冷启动、单次调用、短循环(比如只跑 100 次)根本触发不了编译阈值。
- 默认方法调用次数需 ≥
10000(由-XX:CompileThreshold=10000控制) - 必须经历分层编译:先 C1 快速编译,再等 C2 高度优化编译
- 加
-XX:+PrintCompilation看到类似12345 b com.example.Test::buildString (25 bytes)且带coarsened或eliminated日志,才算真正生效 - 用
System.nanoTime()测微基准没意义——JIT 还没热起来,测的是解释器性能
什么样的 synchronized 会被真正消除
只有一种典型模式稳定生效:局部 new 出的对象,全程未逃逸,且所有同步操作都在同一栈帧内完成。
- ✅ 安全可消除:
new StringBuffer().append("a").append("b")——StringBuffer的append是 synchronized,但对象没传参、没存 static、没进容器,逃逸分析判定为“线程私有” - ❌ 一写就失效:
logger.info(sb)、list.add(sb)、return sb、甚至sb.toString()(某些 JDK 版本中该方法可能触发 monitor enter 语义) - ⚠️ 注意:锁对象必须是新分配的堆对象;
synchronized(this)或synchronized(MyClass.class)永远不会被消除——它们天然可被其他线程访问
怎么验证锁真的被消除了
不能靠看运行速度,得看字节码或 JIT 日志。
- 用
javap -c查看编译后字节码:如果原synchronized块里没了monitorenter和monitorexit,说明已被消除 - 加 JVM 参数:
-XX:+DoEscapeAnalysis -XX:+EliminateLocks -XX:+PrintOptoAssembly(后者需 debug 版 JVM),搜索eliminate或lock相关日志 - 对比开启/关闭锁消除:
-XX:-EliminateLocks强制禁用,再跑相同压测,吞吐量下降明显,说明原先确实发生了消除 - 注意:JDK8 默认开逃逸分析,但 JDK15+ 默认关闭
-XX:-UseBiasedLocking不影响锁消除,它只影响偏向锁
真正难的不是理解“锁能被删”,而是意识到:JIT 的优化结果不体现在源码或逻辑上,而是在字节码层级静默改写。一旦你写的 synchronized 依赖“锁存在”来实现某种语义(比如错误地用它做内存屏障替代 volatile),锁消除就会让程序行为彻底失控——这种 bug 在压测环境才暴露,且无法在 IDE 单步调试中复现。

















