深入浅出 Java 内存模型(JMM)
JMM(Java Memory Model)是 Java 并发编程的基石,但它常常让人感到抽象难懂。本文用"人话+例子"帮你彻底搞懂。
一、JMM 到底在解决什么问题?
先想一个场景:
java
// 线程 A
flag = true;
// 线程 B
while (!flag) { }
问题来了:
- 线程 A 改了
flag,线程 B 一定能马上看到吗? - 编译器会不会把
while(!flag)优化成死循环? - CPU 缓存会不会导致两个线程读到不同的值?
JMM 就是 Java 官方给出的"标准答案" :它定义了多线程之间共享变量的可见性、有序性和原子性规则,屏蔽了不同硬件/操作系统的差异,让 Java 程序在任何平台上都有一致的并发行为。
一句话总结:JMM 规定了线程之间如何通过内存进行通信,以及什么时候一个线程的写操作对另一个线程可见。
二、先理解"抽象内存模型"
JMM 抽象出两层结构:
┌─────────────┐ ┌─────────────┐
│ 线程 A │ │ 线程 B │
│ 工作内存 │ │ 工作内存 │
│ (副本) │ │ (副本) │
└──────┬──────┘ └──────┬──────┘
│ read/write │
▼ ▼
┌─────────────────────────────────────┐
│ 主内存(共享变量) │
└─────────────────────────────────────┘
- 主内存:所有共享变量存在这里
- 工作内存:每个线程有自己的副本(对应 CPU 缓存/寄存器)
线程读写变量,实际是:从主内存拷贝到工作内存 → 修改 → 再写回主内存。这就产生了可见性问题------A 改了主内存,B 的工作内存可能还是旧值。
注意:工作内存是抽象概念,不是真的内存块,它对应 CPU 寄存器、缓存等。
三、三大核心特性
1. 原子性(Atomicity)
一个操作不可分割,要么全做,要么不做。
问题例子:
java
int count = 0;
count++; // 实际是三步:读 → 加1 → 写回,不是原子的!
多线程下 count++ 会丢更新。解决:synchronized、AtomicInteger、Lock。
JMM 只保证基本类型(除 long/double)的读写 是原子的,
i++这种复合操作不保证。
2. 可见性(Visibility)
一个线程修改共享变量后,其他线程能立刻看到。
问题例子就是开头的 flag。解决:
volatile:强制读写主内存,保证可见性synchronized/Lock:释放锁前刷回主内存
3. 有序性(Ordering)
程序执行的顺序按照代码先后顺序执行------但编译器和 CPU 会为了性能做指令重排序,单线程下没问题(保证最终结果一致),多线程下可能出 bug。
经典例子(双重检查锁单例):
java
public class Singleton {
private static volatile Singleton instance; // volatile 关键!
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 这行不是原子的!
}
}
}
return instance;
}
}
instance = new Singleton() 实际分三步:
- 分配内存
- 初始化对象
- 把引用指向内存
如果重排序成 1→3→2,另一个线程可能拿到还没初始化完的对象 。加 volatile 禁止这种重排。
四、Happens-Before 原则(核心!)
JMM 用 happens-before 来定义可见性:如果 A happens-before B,那么 A 的结果对 B 可见。
记住这几条就够了:
| 规则 | 含义 |
|---|---|
| 程序顺序规则 | 同一线程内,前面的操作 hb 后面的操作 |
| 锁规则 | 解锁 hb 后续对同一锁的加锁 |
| volatile 规则 | 对 volatile 变量的写 hb 后续对该变量的读 |
| 线程启动规则 | Thread.start() hb 该线程的所有操作 |
| 线程终止规则 | 线程所有操作 hb 其他线程检测到它终止(join) |
| 传递性 | A hb B,B hb C ⟹ A hb C |
举例理解:
java
int x = 1; // ①
volatile boolean flag = true; // ② 写 volatile
// 另一线程
if (flag) { // ③ 读 volatile
System.out.println(x); // ④ 一定输出 1
}
因为 ① hb ②(程序顺序),② hb ③(volatile 规则),③ hb ④(程序顺序),传递得 ① hb ④,所以 x=1 可见。
happens-before 的本质:它不要求真的"先执行",只要求前者的结果对后者可见。 这给了编译器和 CPU 重排的空间,只要不违反 hb 规则就行。
五、volatile 深入
volatile 是 JMM 最轻量的同步手段,它保证两件事:
- 可见性:写 volatile 变量立刻刷回主内存,读时从主内存读
- 有序性:禁止指令重排序(通过内存屏障 Memory Barrier 实现)
它不保证原子性! volatile int i; i++; 依然不安全。
底层原理:volatile 写前后插入内存屏障,禁止屏障两侧的指令重排,同时触发缓存一致性协议(如 x86 的 MESI)。
适用场景:状态标志位、DCL 单例、一写多读的场景。
六、总结一张图
JMM 三大特性
┌────────┼────────┐
原子性 可见性 有序性
│ │ │
synchronized volatile volatile
Lock 锁 happens-before
Atomic 内存屏障
学习建议:
- 先理解"工作内存/主内存"抽象
- 记住三大特性各自的问题和解法
- 吃透 happens-before 六条规则
- 用 DCL 单例和
flag循环两个例子反复推演
JMM 的精髓在于:它允许重排序和缓存来提升性能,但通过 happens-before 规则保证程序员需要的那部分正确性。理解了这一点,并发编程的很多"坑"就豁然开朗了。