Java 内存模型(JMM)全解析
Java 内存模型(Java Memory Model,JMM)是 JVM 定义的一套抽象规范,而非物理内存结构(如堆、栈、方法区)—— 它解决的是多线程并发场景下,因 CPU 缓存、指令重排序、多核处理器等硬件特性导致的内存访问一致性问题,核心目标是定义线程与主内存之间的交互规则,保障多线程程序的内存可见性、原子性、有序性。
一、先澄清:JMM ≠ JVM 内存结构
很多人会混淆「Java 内存模型(JMM)」和「JVM 运行时数据区(堆、栈、方法区等)」,两者的核心区别:
| 维度 | Java 内存模型(JMM) | JVM 运行时数据区 |
|---|---|---|
| 本质 | 抽象规范(解决多线程内存一致性问题) | 物理内存划分(管理对象、方法、线程栈等存储) |
| 关注对象 | 线程、主内存、工作内存的交互规则 | 堆(共享)、虚拟机栈(私有)、方法区等物理区域 |
| 核心目标 | 保障并发正确性(可见性、原子性、有序性) | 内存分配与回收 |
二、JMM 的核心概念:主内存 & 工作内存
JMM 抽象出「主内存」和「工作内存」两个概念,所有多线程内存交互都基于这两个区域的规则:
-
主内存(Main Memory):
- 所有线程共享的内存区域,存储程序中所有的共享变量(实例变量、静态变量、数组元素等,局部变量/方法参数是线程私有,不存于主内存);
- 是共享变量的「唯一真实数据源」。
-
工作内存(Working Memory):
- 每个线程私有的内存区域,存储线程对主内存共享变量的「副本」;
- 线程对共享变量的所有操作(读、写)必须先加载到工作内存,操作完成后再写回主内存,不能直接操作主内存的变量。
-
JMM 定义的变量交互规则(8 种原子操作):
JMM 规定了线程操作共享变量的 8 种基本操作,且每种操作必须满足原子性、可见性约束:read:从主内存读取变量到工作内存;load:将read读取的变量值加载到工作内存的变量副本中;use:将工作内存的变量值传递给线程的执行引擎(如计算);assign:将执行引擎的结果赋值给工作内存的变量副本;store:将工作内存的变量值写入主内存;write:将store写入的变量值更新到主内存的变量中;lock:锁定主内存的变量(仅能被一个线程锁定);unlock:解锁主内存的变量。
三、JMM 要解决的三大核心问题
多线程并发时,仅靠「主内存-工作内存」的基础规则无法保证正确性,因为会出现可见性、原子性、有序性 问题,JMM 通过规范和关键字解决这些问题。
问题1:可见性
定义
一个线程修改了共享变量的值,其他线程能「立刻看到」这个修改后的值。
产生原因
线程修改共享变量时,先修改工作内存的副本,并未立即写回主内存;其他线程仍读取自己工作内存的旧副本,导致“看不到”最新值。
JMM 的解决方式
volatile:强制变量的写操作立刻刷回主内存,读操作直接从主内存加载(跳过工作内存缓存),保证可见性;synchronized:解锁时会将工作内存的修改刷回主内存,加锁时会清空工作内存,重新从主内存加载变量;final:被final修饰的变量初始化完成后,对所有线程可见(不可修改,天然可见)。
代码示例:volatile 保证可见性
/**
* 可见性问题演示:不加 volatile,线程2看不到线程1修改的 flag
*/
public class VisibilityDemo {
// 共享变量:不加 volatile 时,线程2无法感知修改
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// 线程1:修改 flag 为 true
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("线程1:flag 已改为 true");
}).start();
// 线程2:循环读取 flag,直到为 true 才退出
new Thread(() -> {
while (!flag) {
// 不加 volatile 时,此处会无限循环(线程2读取的是工作内存的旧值)
// 加 volatile 后,能立刻看到线程1的修改,退出循环
}
System.out.println("线程2:感知到 flag 为 true,退出循环");
}).start();
}
}
- 不加
volatile:线程2会无限循环(工作内存缓存旧值); - 加
volatile:线程1修改后立刻刷回主内存,线程2读取主内存最新值,退出循环。
问题2:原子性
定义
一个操作(或多个操作)要么全部执行且不被中断,要么完全不执行,中间状态不会被其他线程感知。
产生原因
JMM 仅保证「基本数据类型的读取/赋值(如 int a = 10)」是原子操作,但复合操作(如 i++、a += b) 拆分为多个步骤,会被线程切换打断。
JMM 的解决方式
synchronized:通过加锁保证代码块的原子性(同一时间仅一个线程执行);java.util.concurrent.atomic原子类(如AtomicInteger):基于 CAS 指令实现无锁原子操作;Lock锁(如ReentrantLock):手动控制锁的获取/释放,保证原子性。
代码示例:i++ 的原子性问题
import java.util.concurrent.atomic.AtomicInteger;
/**
* 原子性问题演示:i++ 是非原子操作,多线程下结果错误
*/
public class AtomicityDemo {
// 普通变量:多线程 i++ 会出错
private static int count = 0;
// 原子类:保证自增操作原子性
private static AtomicInteger atomicCount = new AtomicInteger(0);
// 非原子操作:i++ = 读取count + 加1 + 写回count
public static void increment() {
count++;
}
// 原子操作:CAS 实现自增
public static void atomicIncrement() {
atomicCount.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
// 1000个线程,每个执行1000次 increment
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increment();
atomicIncrement();
}
}).start();
}
// 等待所有线程执行完成
Thread.sleep(2000);
System.out.println("普通count结果:" + count); // 小于1000000(原子性问题)
System.out.println("原子类count结果:" + atomicCount); // 等于1000000(原子性保证)
}
}
count++拆分为「读、加、写」三步,线程切换会导致值被覆盖,结果小于 1000000;AtomicInteger的incrementAndGet()基于 CAS 实现原子自增,结果准确。
问题3:有序性
定义
程序执行顺序「按代码书写顺序执行」,但编译器/CPU 为优化性能会进行「指令重排序」,单线程下重排序不影响结果,多线程下会导致执行顺序混乱。
产生原因
- 编译器重排序:Java 编译器在不改变单线程语义的前提下,调整语句执行顺序;
- CPU 重排序:CPU 为提升并行度,调整指令执行顺序。
JMM 的解决方式
volatile:禁止指令重排序(通过内存屏障实现);synchronized:保证代码块内的执行顺序与代码顺序一致;final:禁止final变量的写操作与构造函数重排序(避免半初始化对象)。
代码示例:DCL 单例的有序性问题
/**
* 有序性问题演示:DCL单例(Double Check Lock)不加 volatile 的风险
*/
public class Singleton {
// 不加 volatile:可能出现半初始化对象(指令重排序导致)
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查:避免不必要的锁
if (instance == null) {
synchronized (Singleton.class) {
// 第二次检查:防止多线程同时进入第一个if
if (instance == null) {
// new Singleton() 拆分为3步:
// 1. 分配内存 2. 初始化对象 3. 赋值给instance
// 不加 volatile 时,CPU可能重排序为 1→3→2,导致其他线程读取到“半初始化对象”
instance = new Singleton();
}
}
}
return instance;
}
}
- 不加
volatile:new Singleton()被重排序为「分配内存→赋值→初始化」,线程A执行到「赋值」但未初始化时,线程B第一次检查instance != null,直接返回半初始化的对象,导致空指针; - 加
volatile:禁止instance的写操作重排序,保证「初始化完成后才赋值」。
四、JMM 的核心原则:Happens-Before(先行发生原则)
Happens-Before 是 JMM 定义的「可见性规则」—— 若操作 A 先行发生于操作 B,则 A 的结果对 B 可见,且 A 的执行顺序早于 B。它是判断多线程操作是否存在竞争的核心依据,无需关注底层重排序/缓存,只需判断是否满足以下规则:
| 规则名称 | 规则说明 |
|---|---|
| 程序次序规则 | 单线程内,前面的代码操作先行发生于后面的代码操作(按代码顺序) |
| 管程锁定规则 | 对一个锁的 unlock 操作先行发生于后续对该锁的 lock 操作 |
| volatile变量规则 | 对 volatile 变量的写操作先行发生于后续对该变量的读操作 |
| 线程启动规则 | 线程的 start() 方法先行发生于线程内的所有操作 |
| 线程终止规则 | 线程内的所有操作先行发生于线程的 join() 方法返回 |
| 传递性规则 | 若 A 先行于 B,B 先行于 C,则 A 先行于 C |
| 对象终结规则 | 对象的构造函数执行完成先行发生于其 finalize() 方法执行 |
示例:Happens-Before 规则的应用
public class HappensBeforeDemo {
private static int a = 0;
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// 线程1:写操作
Thread t1 = new Thread(() -> {
a = 1; // 操作1
flag = true; // 操作2(volatile写)
});
// 线程2:读操作
Thread t2 = new Thread(() -> {
if (flag) { // 操作3(volatile读)
System.out.println(a); // 操作4:必输出1
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
- 程序次序规则:操作1 先行于 操作2;
- volatile变量规则:操作2 先行于 操作3;
- 传递性规则:操作1 先行于 操作4;
- 因此,线程2读取到
flag=true时,a=1对线程2可见,输出1。
五、JMM 的关键细节补充
-
long/double 的非原子性协定:
JMM 允许 JVM 将 64 位的long/double变量的读写拆分为两个 32 位操作(读高32位、读低32位),导致多线程下可能读取到“半个值”。但现代主流 JVM(如 HotSpot)已默认保证long/double的原子性,无需额外处理。 -
内存屏障(Memory Barrier):
JMM 通过内存屏障实现volatile的禁止重排序和可见性:- 写
volatile后插入「写屏障」:强制刷回主内存,禁止后续指令重排序到写屏障前; - 读
volatile前插入「读屏障」:清空工作内存,从主内存加载最新值,禁止前面指令重排序到读屏障后。
- 写
六、总结
- JMM 是抽象规范,核心目标是解决多线程的可见性、原子性、有序性问题,而非管理物理内存;
- 核心概念是「主内存(共享)」和「工作内存(私有)」,线程操作共享变量必须通过工作内存;
volatile解决可见性+有序性,synchronized/原子类解决原子性,Happens-Before 是可见性的核心规则;- 理解 JMM 的本质:通过约束线程与主内存的交互,屏蔽硬件层面的缓存、重排序差异,保证多线程程序的跨平台一致性。
更多推荐



所有评论(0)