锁消除和锁粗化

文章目录

🔒 锁消除 & 锁粗化 --- 一文彻底搞懂

这两个都是 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 上用 StringBufferStringBuilder 性能几乎一样的原因------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 在局部变量场景下不会有性能损失,以及不必过度担心循环内的细粒度同步。

相关推荐
一条大祥脚16 小时前
Codeforces Round 1099 (Div. 2) 构造|贪心|图论|还原数组
java·算法·图论
yaoxin52112316 小时前
414. Java 文件操作基础 - 批量压缩与索引:将154首十四行诗高效存储为带目录的二进制文件
java·windows·python
超梦dasgg17 小时前
详细讲解:WebMvcConfigurer 接口
java·开发语言·spring
JAVA社区17 小时前
Java进阶全套教程(三)—— Spring框架核心精讲
java·开发语言·spring·面试·职场和发展·mybatis
彭于晏Yan17 小时前
OkHttp 与 RestTemplate 技术选型对比
java·spring boot·后端·okhttp
金銀銅鐵17 小时前
[Java] 如何理解 class 文件中字段的 descriptor?
java·后端
5008417 小时前
Graph Engine 是什么,为什么需要它
java·人工智能·性能优化·ocr·wpf
未若君雅裁17 小时前
服务雪崩、降级、熔断与服务保护
java·微服务
就叫_这个吧17 小时前
Java实现线程间的通讯--使用synchronized关键字和JUC方式实现
java·开发语言
学习中.........18 小时前
JVM 垃圾回收核心技术、演进全景与生产调优规范
java·jvm·测试工具