jvm锁粗化依赖jit运行时分析热点代码中同一对象连续无竞争的加锁序列,满足对象不变、操作轻量、作用域不逃逸且为热点代码时,将循环内多次加锁合并为一次;锁消除则基于逃逸分析判定对象是否线程私有,若无并发可能则直接移除锁。

JVM 识别循环中重复加锁行为并执行锁粗化,依赖的是即时编译器(JIT)在运行时对字节码和执行热点的深度分析,而不是静态语法扫描。它不看“for 循环”这三个字,而是观察同一对象、连续、高频、无竞争风险的加锁-解锁序列是否构成可合并的模式。
锁粗化的触发条件很具体
JIT 并不会对所有循环内的 synchronized 都做粗化。它需要同时满足几个关键事实:
- 加锁对象是同一个(比如
synchronized(this)或synchronized(lockObj),且lockObj在循环中未被重新赋值) - 同步块内部操作轻量,不包含可能导致线程阻塞、I/O、或显式调用其他同步方法的逻辑
- 循环体未逃逸出当前方法作用域(比如没有把锁对象传给其他线程可见的结构)
- 该方法已被 JIT 判定为热点代码(执行次数足够多,触发 C1/C2 编译)
一旦满足,JIT 就会把原本分散在每次迭代中的 monitorenter/monitorexit 指令,重写为仅在循环开始前加一次锁、循环结束后释放一次锁——相当于把锁的边界从“每次迭代”拉长到“整个循环”。
锁消除则走另一条路径:逃逸分析 + 锁无关性判定
它不关心是否在循环里,而专注一个核心问题:这个被锁的对象,有没有可能被别的线程看到?
例如:
public String build() {
StringBuffer sb = new StringBuffer(); // 局部变量,在栈上分配
sb.append("a");
sb.append("b");
return sb.toString();
}
JIT 通过逃逸分析确认:sb 的生命周期完全局限在当前线程栈帧内,没被赋值给任何静态字段、没作为参数传给外部方法、也没被发布到线程共享容器中。既然没有并发访问可能,那 StringBuffer.append() 内部的 synchronized 就纯属冗余。JIT 直接抹掉这些锁指令,让这段代码实际运行效果等同于用 StringBuilder。
两者不是互斥,而是互补的优化视角
- 锁粗化解决的是“锁太多太碎”的问题,目标是减少锁操作次数
- 锁消除解决的是“锁根本不需要”的问题,目标是彻底去掉锁开销
它们都发生在 JIT 编译阶段(通常是服务端 C2 编译器),开发者无需手动干预,但写出利于分析的代码有帮助:避免在循环中反复锁定同一对象、少用非必要的线程安全类(如单线程场景用StringBuilder替代StringBuffer)、减少对象逃逸。
不复杂但容易忽略










