文章目录
- [🔒 锁消除 & 锁粗化 --- 一文彻底搞懂](#🔒 锁消除 & 锁粗化 — 一文彻底搞懂)
🔒 锁消除 & 锁粗化 --- 一文彻底搞懂
这两个都是 JIT 编译器(HotSpot JVM) 对 synchronized 锁的自动优化技术,不需要你写任何额外代码,编译器会悄悄帮你优化。
一、锁消除(Lock Elimination)
核心思想
如果一个锁对象永远不会"逃逸"到其他线程,那这个锁就是多余的,直接干掉!
场景:你写了锁,但其实根本不需要
java
public String concat(String s1, String s2, String s3) {
// StringBuffer 的 append() 方法内部是 synchronized 的
// 但 sb 是局部变量,只有当前线程能访问!
StringBuffer sb = new StringBuffer();
sb.append(s1); // ← 加锁了
sb.append(s2); // ← 又加锁了
sb.append(s3); // ← 还加锁了
return sb.toString();
}
JVM 的分析过程:
┌─────────────────────────────────────────────────┐
│ JIT 编译器的逃逸分析 (Escape Analysis) │
│ │
│ ❓ sb 这个对象会不会被其他线程访问? │
│ → sb 是局部变量 ✓ │
│ → 没有赋值给共享字段 ✓ │
│ → 没有作为参数传给其他方法 ✓ │
│ → 结论:sb 不会逃逸! │
│ │
│ ✅ 既然只有当前线程用,synchronized 就是废操作 │
│ → **消除所有锁!** │
└─────────────────────────────────────────────────┘
优化后等效代码:
java
// 编译器实际生成的代码(锁全没了):
public String concat(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1); // 无锁!
sb.append(s2); // 无锁!
sb.append(s3); // 无锁!
return sb.toString();
}
// 甚至可能进一步用 StringBuilder 替代 StringBuffer
图解对比
【优化前】 【优化后】
┌─ append() ─┐ ┌─ append() ─┐
│ 🔒 lock │ │ ✅ 无锁 │
└────────────┘ └────────────┘
↓ ↓
┌─ append() ─┐ ┌─ append() ─┐
│ 🔒 lock │ ── JIT 分析 ──→ │ ✅ 无锁 │
└────────────┘ "不会逃逸!" └────────────┘
↓ ↓
┌─ append() ─┐ ┌─ append() ─┐
│ 🔒 lock │ │ ✅ 无锁 │
└────────────┘ └────────────┘
💰 开销:3次加解锁 💰 开销:0
触发条件
| 条件 | 说明 |
|---|---|
| 逃逸分析开启 | -XX:+DoEscapeAnalysis(JDK6u23+ 默认开启) |
| 标量替换/栈上分配 | 对象分配在栈上或拆分为标量 |
| 对象不逃逸 | 局部变量,未暴露给其他线程 |
💡 实战提示 :这就是为什么现代 JVM 上用
StringBuffer和StringBuilder性能几乎一样的原因------JVM 会自动消除StringBuffer内部的锁!
二、锁粗化(Lock Coarsening)
核心思想
与其频繁地加锁→释放→加锁→释放......不如合并成一把大锁,减少开销。
场景:循环里反复加解锁
java
// 你写的代码(或 JIT 解析后的中间代码)
for (int i = 0; i < 10000; i++) {
synchronized (lock) { // 第1次加锁
list.add(i); //
} // 第1次释放
// ...
synchronized (lock) { // 第2次加锁
list.add(i * 2); //
} // 第2次释放
// ... 每次循环都这样!
}
问题在哪?
🔒 lock → unlock → 🔒 lock → unlock → 🔒 lock → unlock → ... (重复10000次!)
↑ ↑ ↑ ↑
每次都有开销!上下文切换、CAS操作、内存屏障...
JVM 优化的结果:
java
// JIT 粗化后:
synchronized (lock) { // 只加一次锁!
for (int i = 0; i < 10000; i++) {
list.add(i);
list.add(i * 2);
}
} // 只释放一次!
图解对比
【优化前 - 碎片化锁】 【优化后 - 粗化为一把锁】
🔒→🔓 🔒→🔓 🔒→🔓 🔒→🔓 🔒→🔓 ┌──────────────────────┐
🔒→🔓 🔒→🔓 🔒→🔓 🔒→🔓 🔒→🔓 │ │
🔒→🔓 🔒→🔓 🔒→🔓 🔒→🔓 🔒→🔓 │ 🔒 整个循环体 │
🔒→🔓 🔒→🔓 🔒→🔓 🔒→🔓 🔒→🔓 │ (只加锁+解锁各1次) │
... (N次) │ │
└──────────────────────┘
开销: O(N) × (加锁+解锁成本) 开销: O(1) × (加锁+解锁成本)
另一个典型场景:连续的同步块
java
// 写出的代码
synchronized (obj) { x++; } // 加锁 → 修改 → 释放
synchronized (obj) { y++; } // 加锁 → 修改 → 释放
synchronized (obj) { z++; } // 加锁 → 修改 → 释放
↓ JIT 粗化 ↓
// 实际执行的
synchronized (obj) { // 加锁一次
x++;
y++;
z++;
} // 释放一次
粗化的边界
⚠️ 不是无限制粗化的! JVM 会判断:
- 相邻的锁必须是同一个对象
- 中间不能有非同步的操作会影响其他逻辑
- 粗化范围通常限于基本块内或简单循环
三、两者对比总结
┌───────────────────┬──────────────────┬──────────────────┐
│ │ 锁消除 │ 锁粗化 │
├───────────────────┼──────────────────┼──────────────────┤
│ 英文名 │ Lock Elimination │ Lock Coarsening │
│ │ │ │
│ 做什么 │ 把没用的锁删掉 │ 把多个小锁合并成 │
│ │ │ 一个大锁 │
│ │ │ │
│ 为什么 │ 对象没有逃逸, │ 频繁加解锁的开销 │
│ │ 锁毫无意义 │ 大于持有锁的时间 │
│ │ │ │
│ 关键技术 │ 逃逸分析 │ 代码模式识别 │
│ (Escape Analysis) │ │ │
│ │ │ │
│ 典型场景 │ StringBuffer 局部 │ 循环内的 sync、 │
│ │ 变量、方法内对象 │ 连续的 sync 块 │
│ │ │ │
│ 效果 │ 减少不必要的同步 │ 减少加/解锁次数 │
├───────────────────┴──────────────────┴──────────────────┤
│ 共同点:都是 JIT 即时编译器的自动优化,开发者无需手动干预 │
└──────────────────────────────────────────────────────────┘
四、一句话记忆法
| 技术 | 口诀 |
|---|---|
| 锁消除 | "没人抢的东西,不用上锁" --- 对象是私有的,锁是多余的 |
| 锁粗化 | "与其反复插拔钥匙,不如一直开着门干完活" --- 合并碎锁为大锁 |
这两个优化都是 HotSpot JVM 在运行时(Runtime)由 C2 编译器完成的 ,属于自适应优化的范畴。理解它们有助于你写出更"对路子"的代码------比如知道 StringBuffer 在局部变量场景下不会有性能损失,以及不必过度担心循环内的细粒度同步。