Java 内存模型(Java Memory Model, JMM)解析

Java 内存模型(Java Memory Model, JMM)深度解析

"Write once, run anywhere" 不仅靠字节码,更靠清晰定义的 Java 内存模型 。

它回答了:在多线程环境下,一个线程对共享变量的写入何时对另一个线程可见?


目录

  1. 为什么需要 JMM
  2. 关键抽象:主内存 & 工作内存
  3. Happens-Before 规则(HB 规则)
  4. 内存屏障与指令重排
  5. volatile、synchronized、final 的 JMM 语义
  6. DCL 问题与 volatile 的救场
  7. Java 9+ 的 JMM 演进(VarHandle、Opaque)
  8. 常见误区与调优建议
  9. 一张图速记

1. 为什么需要 JMM

现象 根源 JMM 作用
缓存不一致 CPU 多级缓存 规定可见性协议
指令重排 / 编译器重排 性能优化 用 HB 规则禁止危险重排
原子性无法保证 非原子读写(long/double) 提供锁或 CAS 语义

没有 JMM,所有并发行为都 "由 JVM 实现细节决定",程序不可移植。


2. 关键抽象:主内存 & 工作内存

text 复制代码
           ┌─────────────┐
           │  主内存      │  所有线程共享的"唯一真相"
           └──────┬──────┘
                  │
    ┌─────────────┴─────────────┐
    │                           │
┌───▼───┐    ┌───▼───┐    ┌───▼───┐
│工作内存A│    │工作内存B│    │工作内存C│ 每个线程私有
└───┬───┘    └───┬───┘    └───┬───┘
    │            │            │
  CPU0          CPU1         CPU2
  • 工作内存:线程私有的高速缓存/寄存器副本。
  • 主内存:真正的堆上对象字段。
  • 线程对变量的所有操作都必须先复制到工作内存,再刷回主内存。
  • 带来 可见性问题 :A 线程写入 x=1,B 线程看到的仍是 x=0。

3. Happens-Before 规则(JSR-133 核心)

HB 关系是 偏序 ;若 A hb B,则 A 的写对 B 可见,且禁止编译器把 B 重排到 A 之前。

  1. 程序次序规则

    单线程内,代码书写顺序 happens-before 其后的操作。

  2. 锁规则

    解锁 hb 随后的加锁。

  3. volatile 规则
    volatile 写 hb 任意后续 volatile 读。

  4. 线程启动规则
    Thread.start() hb 该线程的所有动作。

  5. 线程终止规则

    线程所有动作 hb 任何检测到该线程结束的代码(t.join() 返回)。

  6. 中断规则

    对线程 interrupt() hb 检测到中断抛出 InterruptedException。

  7. 终结器规则

    构造函数完成 hb finalize() 开始。

  8. 传递性
    A hb B 且 B hb C ⇒ A hb C。


4. 内存屏障与指令重排

JMM 在底层映射为 CPU 内存屏障(Fence):

屏障类型 作用
LoadLoad 禁止读读重排
StoreStore 禁止写写重排
LoadStore 禁止读写重排
StoreLoad 全能型屏障,成本最高(x86 唯一)

HotSpot 在 volatile 写后插 StoreLoad,在读前插 LoadLoad+LoadStore。


5. 关键字语义一览

特性 可见性 原子性 可重排序 典型场景
volatile ✅ 单次读/写 ❌ 状态标志、DCL 单例
synchronized ✅ 代码块 ❌ 临界区、复合操作
final ✅(构造函数完成后) 初始化 ❌ 不可变对象、安全发布

5.1 volatile 深度

java 复制代码
class Counter {
    volatile int count = 0;
    void inc() { count++; }   // 仅保证可见,不保证原子!
}
  • 读/写操作本身会插入屏障;复合操作需配合 AtomicInteger。

5.2 synchronized 的 JMM 视角

java 复制代码
synchronized(lock) {
    // 进入:Load屏障,使工作内存失效
    // 离开:Store屏障,把修改强刷回主内存
}
  • 解锁时强制刷回,加锁时强制失效,天然 互斥+可见。

5.3 final 安全发布

java 复制代码
class Immutable {
    final int x;
    public Immutable(int x) { this.x = x; } // 写 final
}
// 正确发布
Immutable ref = new Immutable(42);  // 写 ref
  • 写 final 与写 ref 禁止重排 ;其他线程读 ref.x 一定看到 42。

6. Double-Checked Locking 问题 & 解法

java 复制代码
// 有问题的 DCL
class Singleton {
    private static Singleton instance;
    public static Singleton get() {
        if (instance == null) {          // 第一次检查
            synchronized(Singleton.class) {
                if (instance == null)    // 第二次检查
                    instance = new Singleton(); // 可能重排到写引用之前
            }
        }
        return instance;
    }
}

问题:写对象与写引用可能被重排,导致返回 半初始化对象 。

解法:把 instance 声明为 volatile,禁用重排。


7. Java 9+ 的演进

  • VarHandle :提供 CAS、内存屏障、原子数组操作,绕过 sun.misc.Unsafe。
  • Opaque 模式:仅禁止编译器重排,不做 CPU 屏障,用于极致性能场景。
  • JEP 188:标准化多线程语义测试(jcstress)。

8. 常见误区 & 调优建议

误区/做法 正确理解
使用 volatile 保证复合操作原子 需 Atomic* 或锁
认为 synchronized 一定慢 偏向锁/轻量级锁常接近无锁
把 HashMap 替 ConcurrentHashMap 就安全 只保证结构安全,复合逻辑仍需同步
滥用 System.gc() 解决内存可见 与 JMM 无关,甚至会引入额外 STW

建议

  1. 尽量用 java.util.concurrent 高级工具 ,不要裸写 wait/notify。
  2. 共享可变变量统一用 volatile 或锁;不可变对象最安全。
  3. 压测时打开 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly 查看屏障指令。

9. 一张图速记

graph TD A[Java Code] --> B(编译器/CPU 重排?) B -->|禁止| C{Happens-Before} C --> D((volatile write<br/>synchronized unlock<br/>Thread.start...)) D --> E(内存屏障<br/>LoadStore/StoreLoad...) E --> F(可见性保证)

结语

JMM 不是 JVM 的实现细节,而是给所有并发程序员的"契约" 。

写出正确高效的并发代码,关键是让代码满足 HB 关系,而非迷信特定硬件行为。

相关推荐
IT_陈寒2 小时前
Java线程池这破玩意,差点让我周末加班排查到凌晨
前端·人工智能·后端
弈栈录3 小时前
Java AI 应用的异步化与高并发设计
java·后端·架构
凤山老林3 小时前
Spring Boot 集成 iText 7 实现动态 PDF 生成与电子签章:合同、报表场景实战
spring boot·后端·pdf·itext7·电子签章
孙启超3 小时前
【AI开发之Rust】第 19 课:UniFFI 导出核心能力
开发语言·后端·rust
ServBay4 小时前
基于Jev的浏览器Agent插件狂揽 21k star,3分钟教你解放双手
后端·aigc·ai编程
Json____4 小时前
家居装修 AI 智能咨询助手:让装修咨询从“大海捞针“变成“一问即答“
java·后端·vue3·it学习·wwwoop.com
Apifox5 小时前
Apifox 9 月更新|CLI 能力升级、GitLab 私有化部署接入与产品体验优化
前端·后端·测试
鱼弦5 小时前
Agent智能体 vs 传统运维:职业天花板的3倍差距?
后端
暗夜行者之光5 小时前
LangGraph 实战:用 LangSmith 可视化追踪 AI 智能体执行轨迹
后端
鱼弦5 小时前
Agent 的工具选择策略:从硬编码到动态决策
后端