Java 内存模型(JMM)全面详解
Java 内存模型(Java Memory Model,简称 JMM)是 Java 语言规范中非常核心的一部分,它定义了多线程环境下,Java 程序如何与内存交互的规则。
JMM 并不是描述“JVM 物理内存如何划分”(那是 JVM 内存结构),而是规定了线程与主内存之间抽象的交互规则,以及什么情况下一个线程的写操作对另一个线程可见。
一、为什么需要 Java 内存模型?
现代 CPU 为了性能,引入了多级缓存(L1/L2/L3)、指令重排序、写缓冲区(Store Buffer)、失效队列(Invalidate Queue)等优化。
这些优化在单线程下没问题,但在多线程下会导致可见性、有序性问题。
例子(经典的“双重检查锁定”失败案例):
public class Singleton {
private static Singleton instance; // 没有 volatile
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题在这里
}
}
}
return instance;
}
}
问题:instance = new Singleton() 并不是原子操作,它可能被重排序为:
- 分配内存空间
- 调用构造方法初始化对象
- 把引用指向分配的内存地址(instance 指向了半初始化对象)
线程 A 可能在第 3 步完成时,线程 B 看到 instance ≠ null,但对象还没初始化完成 → 使用到半初始化对象 → 崩溃或逻辑错误。
JMM 存在的意义:定义清晰的“happens-before”规则,让程序员知道什么时候能保证可见性、有序性。
二、JMM 的两大核心:happens-before 与 as-if-serial
1. happens-before 关系(最重要!)
如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。
JMM 保证的 happens-before 规则(记住这 8 条):
| 序号 | 规则名称 | 描述 |
|---|---|---|
| 1 | 程序顺序规则 | 同一个线程内,前面的操作 happens-before 后面的操作(as-if-serial 语义) |
| 2 | 监视器锁规则 | 解锁一个 monitor → 后续对同一个 monitor 加锁 happens-before 它之前的所有操作 |
| 3 | volatile 变量规则 | 对 volatile 变量的写 → 后续对同一个 volatile 变量的读 happens-before 它之前的写 |
| 4 | 线程启动规则 | Thread.start() → 线程内任意操作 happens-before start() 之后 |
| 5 | 线程终止规则 | 线程内所有操作 happens-before 其他线程检测到该线程终止(isAlive()、join() 返回) |
| 6 | 线程中断规则 | interrupt() → 被中断线程收到中断信号 happens-before 中断检测 |
| 7 | 对象终结规则 | 对象初始化完成(构造方法执行完毕)→ finalize() 方法 happens-before 它 |
| 8 | 传递性 | 如果 A hb B 且 B hb C,则 A hb C |
最常用、最核心的三条:
- 程序顺序规则
- 监视器锁规则(synchronized)
- volatile 变量规则
2. as-if-serial 语义
单线程内,只要不改变程序的执行结果,编译器和处理器可以随意重排序。
int x = 1;
int y = 2;
x = x + 3;
y = y * 4;
以上四行可以任意重排,只要最终 x=4, y=8 即可。
但多线程下,重排序会破坏可见性,所以需要 JMM 约束。
三、JMM 如何保证可见性、有序性、原子性
| 特性 | 保证方式 | 典型手段 | 备注 |
|---|---|---|---|
| 原子性 | JMM 只保证基本类型的赋值是原子的 | synchronized、Lock、AtomicXXX | long/double 的读写在 32 位 JVM 可能非原子 |
| 可见性 | happens-before + 内存屏障 | volatile、synchronized、final、Lock | 写完 volatile 立即刷新到主存 |
| 有序性 | happens-before + 禁止重排序 | volatile、synchronized、Lock | volatile 写后读建立 happens-before |
四、volatile 关键字(JMM 的核心工具)
volatile 是轻量级同步机制,它保证:
- 可见性:写立即刷新到主存,读从主存取最新值
- 禁止指令重排序:前后建立内存屏障,防止重排序破坏语义
volatile 的内存语义(非常重要):
- 写 volatile 变量:相当于把当前线程本地内存的所有共享变量刷新到主存(StoreStore + StoreLoad 屏障)
- 读 volatile 变量:相当于把当前线程本地内存失效,后续读都从主存取(LoadLoad + LoadStore 屏障)
经典用法:
- 状态标志(开关)
- 单次发布(一次写多次读)
- 配合双重检查锁定(JDK 5+)
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 安全了
}
}
}
return instance;
}
五、synchronized 与 volatile 对比
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | 保证(进入/退出同步块) | 不保证 |
| 可见性 | 保证(解锁前刷新,进锁后失效) | 保证 |
| 有序性 | 保证(建立 happens-before) | 保证(禁止重排序) |
| 使用场景 | 需要互斥、复合操作 | 状态标志、单次发布 |
| 性能开销 | 相对较高(锁竞争时) | 极低(无锁) |
| 阻塞 | 会阻塞 | 不阻塞 |
六、final 关键字在 JMM 中的特殊语义
final 字段在构造方法执行完毕后,对其他线程是可见且不可变的(前提:this 没有逸出)。
public class FinalExample {
private final int x;
private int y;
public FinalExample() {
x = 3; // final 字段初始化
y = 4;
}
// 其他线程看到 x 一定是 3,y 可能不是 4(除非有其他同步)
}
七、常见面试题与答案总结
- volatile 能代替 synchronized 吗?
不能。只有在不需要互斥、只需要可见性+有序性的场景才合适。 - 为什么 long/double 的写不是原子性的?
32 位 JVM 可能把 64 位值拆成两次 32 位写。 - DCL(双重检查锁定)为什么需要 volatile?
防止 new 操作重排序导致半初始化对象被其他线程看到。 - happens-before 和 as-if-serial 的区别?
as-if-serial 是单线程优化,happens-before 是多线程可见性保证。 - volatile 如何实现可见性?
依靠内存屏障(StoreStore、LoadLoad 等)+ 缓存一致性协议(MESI)。
八、总结一句话
Java 内存模型(JMM)本质上是通过 happens-before 规则 + volatile/synchronized/final 等关键字 + 内存屏障,解决了多线程环境下“可见性、原子性、有序性”三大并发编程难题。
掌握 JMM 是真正理解 Java 并发的基础。
想继续深入哪个部分?
- volatile 底层内存屏障实现细节
- JMM 与 happens-before 的完整推导
- 各种并发工具(Atomic、Lock、CAS)的 JMM 语义
- 实际案例分析(单例、状态标志、发布-订阅等)
随时告诉我~