锁消除和锁粗化

文章目录

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

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

相关推荐
JAVA面经实录9173 小时前
Java企业级工程化·终极完整版背诵手册(无遗漏、全覆盖、面试+落地通用)
java·开发语言·面试
许彰午5 小时前
CacheSQL(二):主从复制——OpLog 环形缓冲区与故障自动恢复
java·数据库·缓存
Bat U6 小时前
JavaEE|多线程初阶(七)
java·开发语言
掌心向暖RPA自动化8 小时前
如何获取网页某个元素在屏幕可见部分的中心坐标影刀RPA懒加载坐标定位技巧
java·javascript·自动化·rpa·影刀rpa
日取其半万世不竭8 小时前
Minecraft Java版社区服务器搭建教程(Linux,适合新手)
java·linux·服务器
TeamDev9 小时前
JxBrowser 9.0.0 版本发布啦!
java·前端·混合应用·jxbrowser·浏览器控件·跨平台渲染·原声输入
AI人工智能+电脑小能手9 小时前
【大白话说Java面试题】【Java基础篇】第24题:Java面向对象有哪些特征
java·开发语言·后端·面试
AI人工智能+电脑小能手10 小时前
【大白话说Java面试题】【Java基础篇】第25题:JDK1.8的新特性有哪些
java·开发语言·后端·面试
likerhood10 小时前
SLF4J: Failed to load class “StaticLoggerBinder“ 解决
java·log4j·maven
早日退休!!!10 小时前
大模型推理瓶颈七层分析模型
java·服务器·数据库