同步消除消的是无竞争场景下的同步开销,即jit在逃逸分析确认对象未逃逸后跳过加锁/解锁逻辑,而非删除synchronized关键字。

同步消除到底消的是什么锁
同步消除(Lock Elimination)不是删掉 synchronized 关键字,而是 JIT 编译器在运行时发现:某个加了锁的对象,根本不会被其他线程看到,那这把锁就纯属摆设——直接跳过加锁/解锁逻辑。典型例子就是方法内创建的 StringBuffer:它只在当前方法里被 append() 几次,没传出去、没存静态字段、也没进线程共享容器,JVM 就敢把它身上的锁“摘掉”。
- 消的是「无竞争场景下的同步开销」,不是语法层面的锁声明
- 前提是逃逸分析已启用且能确认对象未逃逸;关掉
-XX:+DoEscapeAnalysis,StringBuffer就会老老实实走同步路径,比StringBuilder慢 15% 左右(JMH 实测) - 不是所有
synchronized都能消:如果对象被赋值给static字段、放入ConcurrentHashMap、或作为返回值传出,逃逸成立,锁保留
怎么验证你的代码真被同步消除了
光看吞吐量提升不保险,得看 JVM 是否真的跳过了锁逻辑。最直接的方式是用 -XX:+PrintAssembly(需 hsdis),但门槛高;更实用的是组合以下三步:
- 加参数
-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation,观察方法是否被 JIT 编译为 C2(server compiler)版本——只有 C2 才做逃逸分析和锁消除 - 用
-XX:+PrintOptoAssembly或 JMH 的@Fork(jvmArgsAppend = "-XX:+PrintOptoAssembly")查看编译后汇编,搜索lock前缀指令;若没出现,说明锁已被省略 - 对比开启/关闭逃逸分析的性能差异:加
-XX:-DoEscapeAnalysis后跑同一基准测试,若synchronized方法耗时明显上升,基本可反推原先是被消了
为什么有时开了逃逸分析,锁还是没消除
逃逸分析不是魔法开关,它受限于分析粒度和代码结构。常见失效场景包括:
- 对象被间接引用:比如
new ArrayList().add(user)后把 list 传入一个泛型方法,JVM 可能无法精确追踪user是否逃逸 - 使用反射或 JNI:JIT 编译器对这类动态行为保守处理,直接放弃分析
- 方法太长或含大量分支:C2 编译阈值默认 10000 字节,超限可能退化为 C1 编译,而 C1 不做锁消除
- 调试模式干扰:加了
-agentlib:jdwp或 IDE 远程调试时,部分逃逸优化会被禁用
重构代码让同步消除更容易生效
与其等 JVM 猜,不如把意图写清楚。关键原则是:让对象生命周期封闭、路径可预测。
立即学习“Java免费学习笔记(深入)”;
- 避免返回新对象实例:
return new OrderResult(...)容易逃逸;改用拆解参数 + 返回基本类型,如return buildResult(long itemId, int quantity, boolean valid) - 慎用集合包装:不要写
List<user> users = new ArrayList(); users.add(new User()); process(users);</user>;改为循环体内直接处理new User(),或用数组+长度控制 - 局部变量命名不误导:别用
cache、global、holder这类词命名局部变量,虽然不影响功能,但可能干扰你自己的代码审查节奏 - 注意 Lambda 捕获:
Supplier<user> s = () -> new User(); s.get();</user>中的User会被视为可能逃逸(因函数式接口实例可能跨线程复用)
逃逸分析本身不保证 100% 消锁,它只是个概率游戏——你写的越“规矩”,JVM 越敢下注优化。真正难的不是调参,是让代码的语义边界比 JVM 的分析能力更清晰。











