Java 内存模型 JMM
什么是 JMM?
JMM(Java 内存模型)是 Java 规范中定义的多线程访问共享变量的规则集合,它屏蔽了不同 CPU 架构的内存差异,给 Java 程序员提供一个统一的内存视图。

核心问题:线程 A 把 x 改成 42 写入自己的工作内存,但还没刷回主内存时,线程 B 读到的 x 仍是旧值 0。JMM 定义了什么时候必须刷新、什么时候可以看见对方的修改。
什么是可见性、原子性、有序性?
线程安全的三个核心维度,每个都对应不同的问题和解决方案:
原子性
一个操作要么全部执行,要么完全不执行,中间不被打断。
- 单次读/写基本类型(long/double 除外)是原子的
- i++ 不是原子的(读-改-写三步)
- 解决:synchronized、AtomicInteger
可见性
一个线程对共享变量的修改,其他线程能立即看到最新值。
- 问题根源:线程操作的是工作内存副本
- CPU 缓存和指令重排都可能导致不可见
- 解决:volatile、synchronized
有序性
程序执行的顺序符合代码书写的顺序,不被重排。
- 编译器和 CPU 会对指令重排序以提高性能
- 单线程内重排不影响结果,但多线程下会出问题
- 解决:volatile(内存屏障)、synchronized
// 有序性问题经典案例 — 双重检查锁(DCL)
public class Singleton {
// 必须加 volatile,禁止指令重排
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查:避免不必要的同步
if (instance == null) {
synchronized (Singleton.class) {
// 第二次检查:防止多次创建
if (instance == null) {
instance = new Singleton();
// new Singleton() 分三步:
// 1. 分配内存
// 2. 初始化对象(调用构造方法)
// 3. 将引用赋给 instance
//
// 没有 volatile 时,可能重排为 1→3→2
// 线程 B 拿到 instance 时,对象还未初始化!
}
}
}
return instance;
}
}什么是 happens-before?
happens-before 是 JMM 中定义"操作可见性"的核心规则。如果操作 A happens-before 操作 B,则 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。
它不是说 A 一定先于 B 物理执行,而是说"从 B 的视角看,A 的结果一定可见"。JMM 定义了 8 条基本规则:
程序顺序规则
同一线程中,前面的操作 happens-before 后面的操作。
int a = 1; // 操作1
int b = a; // 操作2 — 保证读到 a=1volatile 变量规则
对一个 volatile 变量的写操作 happens-before 后续对它的读操作。
volatile boolean ready = false;
// 线程A写
ready = true; // 写
// 线程B读
if (ready) { ... } // 一定能读到 true锁规则(监视器锁)
对一个锁的解锁(unlock)happens-before 后续对这个锁的加锁(lock)。
synchronized(lock) {
x = 42; // 写
} // unlock
// 另一线程
synchronized(lock) {
// lock 后,x 一定是 42
}线程启动规则
Thread.start() happens-before 被启动线程中的任何操作。
x = 100;
t.start(); // start() 之前的写对新线程可见
// 新线程中
// x 一定是 100线程终止规则
线程中所有操作 happens-before 其他线程检测到该线程终止(join() 返回等)。
// 线程T中
x = 200;
// 主线程
t.join();
// join() 返回后,x 一定是 200线程中断规则
对线程调用 interrupt() happens-before 被中断线程检测到中断。
t.interrupt();
// 被中断线程中
// Thread.interrupted() 返回 true对象终结规则
一个对象初始化完成(构造函数执行结束)happens-before 它的 finalize() 方法的开始。
// 构造完成 happens-before finalize()传递性
若 A happens-before B,且 B happens-before C,则 A happens-before C。
// A hb B, B hb C → A hb C
// 这是推导复杂 happens-before 的基础happens-before 是 JMM 对程序员的承诺——只要你的代码满足这些规则,JVM 保证内存可见性和有序性,不用关心底层 CPU 缓存细节。
volatile 为什么能保证可见性?
可见性问题的根源是 CPU 多级缓存:每个核心都有自己的 L1/L2 缓存,线程读写的是缓存中的副本,不会立即同步到主内存。

volatile 通过插入内存屏障(Memory Barrier)实现:
写操作:StoreStore + StoreLoad 屏障
写入 volatile 变量后,JVM 强制将值刷新到主内存,并让其他所有 CPU 核心的缓存行失效(Cache Invalidation)
读操作:LoadLoad + LoadStore 屏障
读取 volatile 变量前,JVM 强制从主内存重新加载最新值,跳过本地缓存副本
volatile 写 → 刷入主内存 + 使其他缓存失效;volatile 读 → 强制从主内存加载。底层依赖 CPU 的 MESI 缓存一致性协议和内存屏障指令(如 x86 的 LOCK 前缀)。
指令重排序是什么?
编译器和 CPU 为了提高执行效率,会在不影响单线程结果的前提下,对指令进行重新排序。但多线程下,这会导致另一个线程看到"中间状态"。
重排序如何在多线程中制造 Bug




双重检查锁为什么要加 volatile?
双重检查锁(DCL)是单例模式的经典写法,目的是减少锁竞争。但如果不加 volatile,在多线程下会因指令重排序出现严重 bug。
DCL 代码结构
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton();
}
}
}
return instance;new Singleton() 的真实步骤
// 步骤1:分配内存空间
// 步骤2:初始化对象
// 步骤3:将引用赋给 instanceCPU 可能把步骤2和3交换顺序,变成:分配内存 → 赋引用 → 初始化
重排序后,另一个线程会拿到"未初始化完成"的对象

| 写法 | 代码 | 问题 |
|---|---|---|
| 错误写法(无 volatile) | private static Singleton instance; | 重排序可能导致其他线程拿到半初始化对象 |
| 正确写法(加 volatile) | private static volatile Singleton instance; | volatile 写屏障禁止步骤2和3重排,保证初始化完成后才赋引用 |
volatile 在 DCL 里的作用不是保证可见性(synchronized 已经保证了),而是禁止 new 操作内部的指令重排序,防止其他线程看到未初始化完成的对象。