锁粗化在循环体内基本不会生效,因JIT仅对相邻无分支的连续同步块优化,而循环中执行路径、变量读写、异常等不确定性导致临界区语义难保证,逃逸分析失败亦会阻断该优化。

锁粗化在循环体内基本不会生效
HotSpot JVM 的锁粗化(Lock Coarsening)优化,只对「相邻且无分支的连续同步块」起作用。一旦锁操作出现在 for 或 while 循环体内,JIT 编译器几乎必然放弃粗化——因为每次迭代都可能有不同执行路径、变量读写、异常抛出或外部调用,无法保证临界区语义等价。
- 即使循环体极简单(如仅修改一个局部变量),只要存在循环变量参与条件判断或被
final以外的变量捕获,逃逸分析就可能失败,进而阻断锁粗化 - 常见误判场景:
synchronized (obj) { i++; }被反复写在for (int j = 0; j 里,JVM 不会把它合并成一次大同步块 - 可通过
-XX:+PrintCompilation和-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly观察是否生成了合并后的 monitor-enter/exit 指令(实际极少出现)
逃逸分析关闭时锁粗化直接退场
锁粗化依赖逃逸分析(Escape Analysis)的结果:只有当同步对象被判定为「未逃逸」(即仅在当前线程栈内可见),JIT 才敢把多个细粒度锁合并——否则粗化后可能引发数据竞争或锁升级失效。
-
-XX:-DoEscapeAnalysis会彻底禁用逃逸分析,此时锁粗化优化被连带关闭,哪怕代码结构再“适合”也不会触发 - OpenJDK 17+ 默认开启逃逸分析,但若对象被传递给
Thread.start()、System.out.println()、任意非内联方法参数,或作为静态字段赋值,都会导致逃逸判定失败 - 注意:逃逸分析本身是分层编译阶段(C2)行为,刚启动时解释执行阶段看不到任何粗化效果
替代方案比依赖锁粗化更可靠
与其等待 JVM 偶尔生效的锁粗化,不如主动重构同步边界。尤其在循环中频繁加锁,本质是设计信号——说明临界区划分不合理。
- 优先提取循环外的共享状态变更,例如把
list.add(x)改为先收集到局部ArrayList,循环结束后一次性同步写入 - 对计数类操作,用
LongAdder或AtomicInteger替代synchronized,避免锁开销和粗化不确定性 - 若必须循环内同步,确认对象是否真需要锁:比如
StringBuilder在单线程循环里根本不需要synchronized,用错类型比锁没粗化更伤性能
验证锁是否被粗化不能只看代码形状
很多开发者以为把几个 synchronized 块挨着写就能触发粗化,结果发现同步耗时没降——问题常出在观测方式上。
立即学习“Java免费学习笔记(深入)”;
- 用
jstack看不到粗化痕迹,它只显示运行时锁持有状态;真正要看的是 JIT 编译日志里的coarsened关键字(需-XX:+TraceOptoPipelining等深度选项) - JMH 基准测试中,若循环次数太小(如
@Fork(jvmArgsAppend = "-XX:CompileThreshold=100")配合 50 次循环),C2 可能根本没来得及编译,测的还是解释执行 - 最简单的反证法:把循环体改成空操作,再对比有无锁的吞吐量差异——如果差值等于单次
synchronized开销 × 循环次数,说明根本没粗化
锁粗化不是银弹,它只在非常受限的代码模式下悄悄发生。真正影响性能的,往往是循环内同步的设计意图本身是否合理——这点比琢磨 JVM 会不会合并两个 monitorenter 指令重要得多。











