锁消除和锁粗化

文章目录

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

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

相关推荐
云烟成雨TD2 小时前
Spring AI 1.x 系列【26】结构化输出执行流程
java·人工智能·spring
雪的季节3 小时前
qt信号槽跨线程使用时候的坑
java·开发语言·qt
chh5633 小时前
C++--内存管理
java·c语言·c++·windows·学习·面试
白緢3 小时前
嵌入式 Linux + 内核开发高频问题及排查
java·linux·运维
juniperhan3 小时前
Flink 系列第4篇:Flink 时间系统与 Timer 定时器实战精讲
java·大数据·数据仓库·flink
超级大只老咪3 小时前
一维度前缀和解题通用模板(java)
java·开发语言·算法
历程里程碑3 小时前
1 . Git本地操作:版本控制 跨平台协作 仓库核心
java·开发语言·数据结构·c++·git·gitee·github
hekung3 小时前
maven的lifecycle与idea的run
java·maven
阿维的博客日记4 小时前
为什么 ConcurrentHashMap 采用 synchronized 加锁而不采用ReentrantLock
java·juc