重阳,Java 的内存模型(Java Memory Model,简称 JMM)是并发编程里最核心、最容易让人迷惑的概念之一。
很多人会把它和 JVM 运行时数据区(堆、栈、方法区……)混在一起,其实这是两个完全不同的东西:
| 概念 | 全称 | 关注点 | 主要解决什么问题 | 属于哪个规范 |
|---|---|---|---|---|
| JVM 内存结构 | JVM Runtime Data Areas | 线程运行时内存怎么划分 | 对象放哪、局部变量放哪、GC 范围 | 《Java 虚拟机规范》 |
| Java 内存模型 JMM | Java Memory Model | 多线程下共享变量的可见性、有序性、原子性 | 线程之间变量读写何时可见、能不能重排序 | 《Java 语言规范》JLS 第17章 + JSR-133 |
今天我们重点聊 JMM(Java 内存模型),也就是多线程并发编程的“内存契约”。
1. 为什么需要 JMM?(最本质的原因)
现代 CPU 和编译器为了性能,会做各种激进优化:
- 寄存器缓存(线程本地)
- CPU 缓存(L1/L2/L3)
- 写缓冲区(Store Buffer)
- 失效队列(Invalidate Queue)
- 指令重排序(as-if-serial + 内存乱序)
这些优化在单线程下没问题,但在多线程下会导致“我改了,你看不见” 或 “看到的顺序不对”。
举个最经典的例子(很多人面试都见过):
// 线程1
boolean ready = false;
int number = 0;
void writer() {
number = 42; // ①
ready = true; // ②
}
// 线程2
void reader() {
if (ready) { // ③
System.out.println(number); // 可能打印 0,而不是 42!
}
}
可能的结果:线程2 看到 ready=true,却看到 number=0。
原因:编译器/CPU 把①和②重排序了,或者写缓冲区导致可见性延迟。
JMM 的使命:在不牺牲太多性能的前提下,给程序员提供一套可预测的内存可见性规则。
2. JMM 的核心抽象(工作内存 vs 主内存)
JMM 把每个线程想象成有自己的工作内存(Working Memory),所有共享变量的真身放在主内存(Main Memory)。
线程对变量的读写操作必须经过下面八个原子操作:
| 操作 | 含义 | 从哪里到哪里 |
|---|---|---|
| lock | 把主内存变量标记为线程独占 | 主内存 → 线程 |
| unlock | 释放独占标记 | 线程 → 主内存 |
| read | 从主内存读取变量值 | 主内存 → 工作内存 |
| load | 把 read 的值放入工作内存的变量副本 | — |
| use | 线程使用工作内存中的变量值 | — |
| assign | 线程把值赋给工作内存的变量副本 | — |
| store | 把工作内存变量值传给主内存 | 工作内存 → 主内存 |
| write | 把 store 的值写入主内存的变量 | — |
真实 JVM 实现不一定严格这样分,但逻辑上必须遵守这个模型。
3. JMM 真正承诺的东西:happens-before 关系(最重要!)
JMM 不承诺“一定顺序执行”,而是承诺:
如果 A happens-before B,那么 A 在内存上做的所有修改,在 B 看来都是可见的。
常见的 happens-before 规则(2026 年仍然是这些,没有大改动):
| 序号 | 规则名称 | 描述 | 强度 |
|---|---|---|---|
| 1 | 程序顺序规则 | 单线程内,前面的操作 happens-before 后面的操作 | 强 |
| 2 | 监视器锁规则 | unlock → 同一个锁的后续 lock | 强 |
| 3 | volatile 变量规则 | 对 volatile 变量的写 → 后续对同一个变量的读 | 强 |
| 4 | 线程启动规则 | Thread.start() → 线程内任意操作 | 中 |
| 5 | 线程终止规则 | 线程内所有操作 → Thread.join() 返回 / Thread.isAlive()=false | 中 |
| 6 | 线程中断规则 | interrupt() → 检测到中断(抛异常或 isInterrupted()=true) | 中 |
| 7 | 对象终结规则 | 对象初始化完成 → finalize() | 弱 |
| 8 | 传递性 | A hb B 且 B hb C ⇒ A hb C | — |
最常用的两条:
- volatile 写-读:建立 happens-before
- synchronized 解锁-加锁:建立 happens-before
4. volatile 的真实作用(2025-2026 面试最爱问)
很多人以为 volatile 保证原子性,其实错的。
volatile 真正提供的保证(针对 long/double 以外的变量):
- 可见性:写完立刻刷新到主存,读时强制从主存取(或等价效果)
- 禁止重排序:volatile 写前面的操作不能排到写后面;读后面的操作不能排到读前面
- 写 volatile 变量 ≈ StoreStore + StoreLoad 屏障
- 读 volatile 变量 ≈ LoadLoad + LoadStore 屏障
但它不保证复合操作原子性(如 i++)。
5. final 的内存语义(安全发布)
final 字段在构造器中初始化完成后,后续读这个 final 字段一定能看到正确值(前提是没有 this 逸出)。
这是 JMM 给 immutable 对象最强有力的保证。
6. 快速对比表(面试/理解用)
| 特性 | synchronized | volatile | final(构造完) | AtomicXXX/CAS |
|---|---|---|---|---|
| 原子性 | 是(块) | 否(只单次读写) | — | 是(部分操作) |
| 可见性 | 是 | 是 | 是(字段级别) | 是 |
| 有序性 | 是 | 部分(防重排) | 是(防 this 逸出) | 依赖实现 |
| 使用场景 | 互斥同步 | 状态标志、发布对象 | 不可变对象 | 高并发计数器等 |
7. 小结(一句话版)
JMM 不是真的内存划分,而是 Java 给多线程程序员的一份“契约”:
- 你只要遵守 happens-before 规则
- 我(JVM/编译器/CPU)就保证你的修改对其他线程可见,且不会做破坏顺序的重排序
违背这个契约 → 数据竞争(data race) → 未定义行为(可能崩溃、错乱、死循环、看起来“正常”但其实错)
重阳,你现在对 JMM 最想深入哪个点?
- 双重检查锁单例为什么必须加 volatile?
- happens-before 的传递性举例
- volatile 在 x86/arm 上的真实实现差异
- JMM vs C++ memory model 对比
- 实际代码案例分析
随时说,我们继续深挖~