摘要
Java 内存模型(JMM)是理解并发编程的核心基础。它定义了线程之间如何通过内存交互、JVM 如何处理指令重排、编译器和 CPU 如何影响执行结果。本文将以全景图的方式,系统梳理 JMM 的关键概念、运行机制和实践场景,帮助读者从根本上理解 Java 并发的底层逻辑。
一、为什么需要 JMM?
在单线程环境中,程序员可以"顺序执行思维"来推导结果。但在多线程环境下,情况会复杂得多:
- CPU 优化:现代处理器会乱序执行指令,以提高吞吐量。
- 编译器优化:Java 编译器可能会对字节码进行重排序,保持语义等价但执行顺序改变。
- 缓存机制:线程可能并不直接读写主内存,而是读写各自 CPU 缓存中的副本。
如果没有一套严格的规则,开发者将无法预测多线程程序的执行结果。这套规则就是 Java 内存模型(JMM) 。
二、JMM 的核心目标
JMM 要解决的两个关键问题是:
- 可见性:一个线程修改的变量值,什么时候能被其他线程看到?
- 有序性:在多线程环境下,哪些代码执行顺序必须保持不变,哪些可以被编译器/CPU 调整?
- 原子性:某些操作是否能保证不可分割?
简单说,JMM 是 JVM 与开发者之间的"契约",它保证在特定规则下,程序的并发执行结果是可预测的。
三、JMM 的抽象全景图
我们可以用一张 抽象图 来理解 JMM 的核心概念:

- 主内存:所有共享变量都存放在这里。
- 工作内存:每个线程都有独立的工作内存,存放主内存变量的副本。
- 交互方式:线程之间不直接通信,而是通过主内存实现数据交互。
四、JMM 的 8 种内存交互操作
Java 内存模型定义了 8 种操作,来规范线程与主内存之间的交互:
- lock(锁定) :作用于主内存变量,标识该变量为线程独占。
- unlock(解锁) :释放变量,使其能被其他线程访问。
- read(读取) :从主内存读取变量值到线程工作内存。
- load(载入) :将 read 得到的值放入工作内存变量副本。
- use(使用) :将工作内存中的值传递给执行引擎。
- assign(赋值) :把执行引擎计算的值写入工作内存变量。
- store(存储) :把工作内存变量的值写入主内存。
- write(写入) :将 store 的值最终写入主内存变量。
要点 :这 8 种操作必须成对出现,比如
read
和load
搭配,store
和write
搭配。
五、JMM 与三大特性
JMM 直接对应并发编程的三大特性:
1. 原子性
- JMM 保证 基本数据类型的读写操作是原子性的(long 和 double 在 32 位 JVM 上例外)。
- 对于复合操作(如
i++
),需要通过synchronized
或AtomicInteger
来保证原子性。
2. 可见性
- JMM 规定,线程对变量的修改,必须同步回主内存,其他线程才能看到。
volatile
关键字就是可见性保证的一种手段。
3. 有序性
- 在单线程中,代码顺序看似固定,但编译器和 CPU 可能会重排指令。
- JMM 通过 happens-before 原则 来保证关键有序性。
六、happens-before 原则
JMM 并不禁止所有的重排序,而是通过 happens-before 定义了必要的顺序关系:
- 程序次序规则:一个线程内,按照代码顺序执行。
- 锁规则:unlock 必然发生在后续 lock 之前。
- volatile 规则:对一个 volatile 变量的写,先行发生于后续对该变量的读。
- 传递性:A happens-before B,B happens-before C,则 A happens-before C。
- 线程启动规则:Thread.start() happens-before 线程 run()。
- 线程终止规则:线程的所有操作 happens-before 其他线程检测到它终止。
这套规则为我们推导并发结果提供了理论工具。
七、关键关键字与 JMM 的关系
1. volatile
- 保证变量的 可见性 和 有序性(禁止指令重排)。
- 不保证操作的 原子性。
2. synchronized
- 保证 原子性(临界区互斥)。
- 保证 可见性(进入 synchronized 时,工作内存会清空,从主内存读取最新值)。
- 保证 有序性(临界区内的代码不会被重排到外部)。
3. final
- 保证初始化安全性:一旦对象构造完成,final 字段对所有线程可见。
八、典型案例解析
案例 1:双重检查锁(DCL)单例
错误实现(缺少 volatile):
java
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;
}
}
问题:对象初始化过程可能被指令重排,导致其他线程读到"未完全初始化"的对象。
正确实现:
java
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
案例 2:volatile 的可见性测试
java
class TestVolatile {
private static boolean running = true;
public static void main(String[] args) throws Exception {
new Thread(() -> {
while (running) { }
}).start();
Thread.sleep(1000);
running = false; // 线程可能看不到变化
}
}
解决方案:running
使用 volatile
修饰。
九、实践建议
- 避免过度依赖 JMM :尽量使用
java.util.concurrent
包提供的并发工具(如AtomicXXX
、Lock
、Executor
)。 - 理解 volatile 的边界:适合状态标记,不适合复杂复合操作。
- 锁是大杀器 :
synchronized
在 JDK 1.6 之后优化明显,性能远超过去印象。 - 多线程调试 :使用
jconsole
、jstack
分析线程状态,辅助验证并发逻辑。
十、总结
Java 内存模型(JMM)是并发编程的底层基石。
- 它定义了主内存与工作内存的交互规则;
- 通过
happens-before
保证了关键的有序性; - 借助
volatile
、synchronized
、final
等关键字,开发者可以在复杂的多线程环境下写出正确的代码。