深入理解 Java 内存模型(JMM)
一、JMM有什么作用
在 Java 并发编程中,我们常遇到这样的现象:
- 一个线程修改了变量,另一个线程却看不到更新;
- 没有加锁的操作,偶尔出现诡异的结果;
- 加了
volatile
之后,程序行为突然正常了;- 你以为的“顺序执行”,在 CPU 和编译器眼里可能早已面目全非。
这些现象的背后,都与 Java 内存模型(Java Memory Model, JMM) 密切相关。
JMM 不是“内存结构图”,也不是“JVM 堆栈划分”,而是一套规范 —— 它定义了:
- 多线程环境下,线程如何以及何时可以看到其他线程对共享变量的修改
- 哪些操作是原子的、有序的、可见的
- 编译器和处理器可以对指令做哪些优化,又在什么条件下必须禁止优化
二、JMM 的抽象结构:主内存与工作内存
JMM 定义了一个抽象的内存模型,屏蔽了底层硬件和操作系统的差异。它将内存划分为:
主内存:所有线程共享,存储所有的变量(实例字段、静态字段、数组元素等)
工作内存:每个线程私有,保存该线程使用到的变量的副本
注意:
“主内存” ≠ 物理内存,“工作内存” ≠ CPU 寄存器或高速缓存 —— 它是逻辑概念,是对硬件内存层次结构的抽象。
变量读写流程
线程A:
1. 从主内存读取变量 x → 复制到工作内存
2. 在工作内存中修改 x
3. 将 x 的新值写回主内存 线程
B:
1. 从主内存读取变量 x → 复制到工作内存
2. 使用 x 的值(可能仍是旧值!)
如果线程A修改了 x,但还没写回主内存,线程B就读取 x —— 它看到的就是“过期数据”。这就是可见性问题。
三、JMM 的三大核心特性
1. 原子性
指一个操作是“不可分割”的 —— 要么全部执行,要么全部不执行。
天然原子操作:
基本数据类型的读写
引用类型的读写
非原子操作:
i++
(包含读、改、写三步)
long/double
的非 volatile 读写(32位平台)
解决方案:使用
AtomicInteger
、synchronized
、Lock
等保证复合操作的原子性。
2. 可见性
指当一个线程修改了共享变量的值,其他线程能够立即“看到”这个修改。
普通变量:无可见性保证
线程A修改变量 → 写回主内存的时间不确定 → 线程B可能一直读工作内存中的旧值
如何保证可见性?
方式 | 原理 |
---|---|
volatile 变量 | 写操作立即刷回主内存,读操作强制从主内存加载 |
synchronized | 释放锁前将工作内存变量刷回主内存,获取锁后清空工作内存缓存 |
final 字段 | 构造函数内正确初始化后,对其他线程可见(禁止重排序) |
java.util.concurrent 中的类 | 内部使用 volatile / CAS / 锁保证可见性 |
volatile 不保证原子性!
3. 有序性
指程序执行的顺序按照代码的先后顺序执行。
但是编译器和处理器为了优化性能,会进行指令重排序:
int a = 1; // 语句1
int b = 2; // 语句2
int c = a + b; // 语句3
编译器可能重排为:语句2 → 语句1 → 语句3(不影响单线程结果)
但在多线程环境下,重排序可能导致意外:
// 线程1
context = loadContext(); // ①
inited = true; // ② (inited 是 volatile)// 线程2
while (!inited) { } // ③
doSomething(context); // ④ —— 可能拿到 null!
如果 ① 和 ② 被重排序,线程2可能看到 inited=true
,但 context
还未初始化!
解决方案:volatile
、synchronized
、final
等可禁止特定重排序。
四、happens-before 原则
JMM 使用 happens-before(先行发生) 关系来定义操作之间的可见性和有序性。
如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。
JMM 定义的天然 happens-before 规则:
- 程序顺序规则:同一个线程内,前面的操作 happens-before 后面的操作
- 监视器锁规则:解锁操作 happens-before 后续的加锁操作(同一个锁)
- volatile 变量规则:对 volatile 变量的写操作 happens-before 后续的读操作
- 线程启动规则:
Thread.start()
happens-before 该线程的任何操作 - 线程终止规则:线程中的所有操作 happens-before 其他线程检测到它终止(如
join()
返回) - 线程中断规则:
interrupt()
happens-before 被中断线程检测到中断(如抛出InterruptedException
) - 对象终结规则:对象构造函数结束 happens-before
finalize()
开始 - 传递性:如果 A happens-before B,B happens-before C,则 A happens-before C
五、内存屏障(Memory Barrier)
内存屏障是 CPU 指令,用于控制特定条件下的重排序和内存可见性。
JMM 在底层通过插入内存屏障实现 volatile
、synchronized
等语义:
屏障类型 | 作用 |
---|---|
LoadLoad | 禁止上面的读与下面的读/写重排序 |
StoreStore | 禁止上面的写与下面的写重排序 |
LoadStore | 禁止上面的读与下面的写重排序 |
StoreLoad | 最强屏障,禁止上面的写与下面的读/写重排序(开销最大) |
volatile
写操作后插入 StoreStore
+ StoreLoad
volatile
读操作前插入 LoadLoad
+ LoadStore