1. JMM 概述
JMM(Java Memory Model)是 Java 语言规范中定义的一套内存可见性与操作顺序的规则,用于描述多线程环境下中线程如何访问共享变量。它的核心目标是屏蔽底层硬件和操作系统的内存访问差异,在允许编译器和处理器优化性能的同时,为程序员提供一致的内存可见性和操作顺序的语义。
1.1. 并发编程的三大特性
并发编程具有三大核心特性:原子性(Atomicity)、可见性(Visibility)和有序性(Ordering),它们是理解和编写正确多线程程序的基础。
-
原子性(Atomicity)
一个操作(或一组操作)要么全部执行成功,要么完全不执行,中间不会被其他线程打断。 -
可见性(Visibility)
当一个线程修改了共享变量的值,其他线程能够立即看到这个修改后的最新值。 -
有序性(Ordering)
程序代码的执行顺序,按照开发者编写的顺序执行。但在实际运行中,编译器和处理器可能重排序以优化性能。
1.2. 为什么需要 JMM
现代计算机系统为了性能做了大量优化,这些优化在单线程下无害,但在多线程下会导致“可见性”和“有序性”问题:
- CPU 缓存:每个 CPU 核心都有自己的缓存。线程修改共享变量可能只写入自己的缓存,而没有同步到主存,导致其他线程对共享变量的值的修改不可见。
- 指令重排序:编译器或处理器为了优化性能,可能会改变指令执行顺序。这在单线程下不影响结果,但在多线程下可能导致逻辑错误。
- 跨平台一致性需求:不同 CPU 架构对内存访问的语义不同。Java 要实现“一次编写,到处运行”,就必须在语言层面上抽象出统一的内存模型。
JMM 就是为了在这些复杂的环境下,定义哪些行为是合法的、哪些内存操作必须对其他线程可见,从而在允许优化的同时,保证 Java 多线程程序的行为是可预测的。
2. JMM 的核心概念
JMM 的核心概念包括:主内存与工作内存、八大内存交互操作、重排序、内存屏障以及 happens-before 原则。
2.1. 主内存(Main Memory)与工作内存(Working Memory)
- 主内存:所有线程共享的内存区域,存储所有的实例字段、静态字段和数组元素(即堆内存中的对象数据)。
- 工作内存:每个线程私有的内存区域(可以理解为 CPU 缓存和寄存器),保存了该线程使用到的共享变量的副本。
- 交互规则:
- 线程所有对共享变量的操作(读、写)必须在工作内存中进行,不能直接读写主内存。
- 线程间变量值的传递必须通过主内存完成(从主内存加载 → 工作内存操作 → 写回主内存)。
2.2. 八大内存交互操作
“八大内存交互操作”实际上指的是,在 JMM 规范中明确定义的、用于描述线程如何与主内存交互的八种原子操作。它们构成了 JMM 对“工作内存 ↔ 主内存”数据流动的底层抽象,是理解 Java 并发语义的底层基础。
| 操作 | 含义 |
|---|---|
| lock(锁定) | 作用于主内存,把一个变量标识为一条线程独占状态 |
| unlock(解锁) | 作用于主内存,释放 lock 状态,允许其他线程访问 |
| read(读取) | 从主内存读取变量值到工作内存 |
| load(载入) | 把 read 得到的值放入工作内存的变量副本 |
| use(使用) | 工作内存变量传给执行引擎(如运算) |
| assign(赋值) | 执行引擎结果赋值给工作内存变量 |
| store(存储) | 把工作内存变量传回主内存(准备写入) |
| write(写入) | 把 store 的值正式写入主内存 |
JMM 规范对这八个原子操作定义了以下基本规则:
-
read 和 load 必须成对出现
如果一个变量从主内存被 load,那么必须紧接着执行 load,将值放入工作内存。禁止只 read 不 load、只 load 不 read,确保工作内存中的变量副本总是来源于主内存,防止“幻象值”。 -
store 和 write 必须成对出现
如果要将工作内存的值写回主内存,必须先 store 再 write。禁止只 store 不 write、只 write 不 store,确保主内存的更新一定来自工作内存的合法副本。 -
不允许丢弃最近的 assign 操作
如果工作内存中的变量被 assign 赋值过,那么在线程 exit 或 unlock 前,必须通过 store → write 同步回主内存(除非该变量未被共享)。这条规则保障了,一旦修改了共享变量,最终有机会被其他线程看到(如果没有 volatile 或 synchronized,不保证“何时”刷回,但禁止永远不刷)。 -
不允许无中生有地 use 或 assign
工作内存中的共享变量在首次 use 或 assign 之前,必须先从主内存加载(read → load),防止线程使用“垃圾值”或“未定义值”。 -
lock 必须在 read/load 之前,unlock 必须在 store/write 之后(只有使用 synchronized 同步时才适用)
这条规则支撑的是 synchronized 的内存语义,是 happens-before 原则中“监视器锁规则”的底层实现基础:- 进入 synchronized:清空工作内存中共享变量的副本,强制从主内存重新加载(read → load);
- 退出 synchronized:将修改后的共享变量刷回主内存(store → write)。
2.3. 重排序(Reordering)
重排序是指,编译器、处理器或运行时系统为了优化性能,在不改变单线程执行结果的前提下,对指令的执行顺序进行调整的行为。但在多线程环境下,这种“不影响单线程语义”的重排序,可能会破坏线程见的“可见性”和“有序性”,导致程序出错。
JMM 应对的重排序有以下三种来源:
| 类型 | 发生位置 | 说明 |
|---|---|---|
| 编译器重排序 | Java源码 → 字节码或 JIT 编译时 | 编译器可能会调整代码位置以优化性能 |
| 指令级并行重排序(CPU 乱序执行) | CPU 执行阶段 | 现代 CPU 采用乱序执行(Out-of-Order Execution),只要不改变单线程结果,可以任意调度指令 |
| 内存系统重排序 | CPU 缓存 ↔ 主内存 | 写缓冲区(Store Buffer)、无效队列(Invalidate Queue)等机制可能导致写操作对其他 CPU 核心“延迟可见” |
JMM 的目标不是禁止所有重排序,而是通过规则约束哪些重排序是允许的,哪些必须禁止,从而在性能和正确性之间取得平衡。
2.4. 内存屏障(Memory Barrier / Fence)
内存屏障(Memory Barrier,也称 Memory Fence)是一种由硬件或编译器提供的同步指令,用于控制 CPU 或编译器对内存操作的重排序行为,并确保特定内存操作的可见性和执行顺序。JMM 通过插入内存屏障来禁止特种类型的处理器重排序,这是实现 volatile、synchronized、final 等语义的底层机制。
常见的内存屏障操作有以下四类:
| 屏障类型 | 作用 |
|---|---|
| LoadLoad | 禁止屏障前后的读操作(Load)被重排序 |
| StoreStore | 禁止屏障前后的写操作(Store)被重排序 |
| LoadStore | 禁止读操作被重排序到写操作之后 |
| StoreLoad | 禁止写操作被重排序到读操作之前 |
2.5. happens-before 原则
happens-before 原则是 JMM 中最核心的语义规则,用于定义多线程下操作之间的可见性和顺序性保证。
如果操作 A 和 B 满足:A happens-before B,那么:
- A 的执行结果对 B 是可见的;
- A 的执行顺序在 B 之前;
- A 和 B 之间的相关内存操作不能被重排序(编译器/CPU 必须遵守顺序)。
JMM 明确定义了以下 8 种 happens-before 关系:
-
程序顺序规则(Program Order Rule)
在同一个线程内,按照代码顺序,前面的操作 happens-before 后面的操作。
值得注意的是,这并非禁止重排序。只要不影响单线程的结果,编译器/CPU 仍然可以重排序。只有两个操作存在数据依赖或同步依赖时,happens-before 才真正约束行为。 -
监视器锁规则(Monitor Lock Rule)
对一个监视器(Monitor)的解锁(unlock)happens-before 后续对该监视器的加锁(lock)。
也就说是,加锁(lock)操作之前,必须确保监视器(Monitor)已经解锁(unlock)。这是 synchronized 保证可见性的原理。 -
volatile 变量规则(Volatile Variable Rule)
对一个 volatile 变量的写操作 happens-before 后续对该变量的读操作。
这是 volatile 保证自身可见性的原理。此外,volatile 还可以通过 happens-before 关系传递普通变量的可见性。 -
线程启动规则(Thread Start Rule)
Thread.start() 的调用 happens-before 新线程中的任何操作。主线程在 start() 的写,对子线程可见。 -
线程终止规则(Thread Join Rule)
线程中的所有操作 happens-before 其他线程成功调用 join() 返回。用于等待线程完成并获取结果。 -
中断规则(Interrupt Rule)
对线程调用 interrupt() happens-before 被中断线程检测到中断。确保线程的中断能被感知。 -
终结器规则(Finalizer Rule)
对象的构造函数结束 happens-before 它的 finalize() 方法开始。 -
传递性(Transitivity)
如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
如果有 happens-before,则禁止重排序(JVM 插入内存屏障),反之如果没有 happens-before,则允许任意的重排序。因此,happens-before 是 JMM 判断“是否允许重排序”的唯一标准。
3. JMM 机制的底层原理
3.1. Java 程序编译成机器码的过程
-
编写 Java 源代码
开发者使用 .java 文件编写程序,例如 HelloWorld.java。 -
编译为字节码(Bytecode)
使用 javac(Java 编译器)将 .java 源文件编译成字节码(.class 文件)。字节码是一种中间形式的、平台无关的指令集,它不是机器码,不能直接被 CPU 直接执行。字节码设计用于在 Java 虚拟机(JVM)上运行。 -
JVM 加载并执行字节码
当运行程序时,JVM 按顺序执行以下步骤:类加载(Java 的类加载器将 .class 文件加载到内存) → 字节码验证(确保字节码符合 JVM 规范,防止恶意或错误代码) → 解释执行(JVM 内置的解释器逐条读取字节码并执行,这种方式较慢) → 、即时编译 JIT(为了提升性能,现代 JVM 会监控热点代码,并将这些字节码动态编译成本地机器码,这个过程是动态执行的)。 -
执行机器码
一旦 JIT 编译完成,CPU 就可以直接执行这些机器码,获得接近原生程序(如 C++)的性能。
3.2. 指令重排序
指令重排序(Instruction Reordering)是现代计算机系统中为了提升性能而广泛采用的一种优化技术。它可以在编译器层面、CPU 层面,甚至在运行时(JIT 编译器)发生。指令重排序的目的,是在不改变单线程程序语义的前提下,允许编译器、处理器或运行时系统对指令的执行顺序进行重新排序,以提高执行效率。但在多线程下,重排序可能破坏程序的可见性和有序性。
-
编译器重排序
在 Java 编译为字节码或本地机器码时,编译器可能会调整语句顺序。其中,javac 通常不做激进的重排序,但 JIT 编译器(C1/C2)会进行大量优化重排。 -
CPU 指令重排序
现代 CPU 采用乱序执行、流水线、多发射等调度策略,会动态调整指令执行顺序,只要不违反数据依赖。 -
内存系统重排序
- 写缓冲区(Store Buffer)、缓存一致性协议可能会导致写操作对其他 CPU 核心“延迟可见”。
- 即使 CPU 按序执行,其他线程也可能看到“乱序”的内存更新。
3.3. 内存屏障
内存屏障是计算机体系结构和并发编程中的一个核心概念。现代系统为了提升性能,在编译器、CPU 执行、内存系统三个层面进行指令的重排序。这些优化在单线程下是安全的,但在多线程语义下,会影响程序的可见性和有序性。内存屏障就是用来限制这些优化的边界,强制某些内存操作按特定顺序完成,并对其他线程可见。
根据作用不同,内存屏障通常分为以下几类:
| 类型 | 说明 |
|---|---|
| LoadLoad | 确保屏障前的读操作先于屏障后的读操作完成 |
| StoreStore | 确保屏障前的写操作先于屏障后的写操作完成 |
| LoadStore | 确保屏障前的读操作先于屏障后的写操作完成 |
| StoreLoad | 确保屏障前的写操作完成并对其他 CPU 可见后,才执行屏障后的读操作 |
注意:StoreLoad 屏障通常也被称为“全屏障”,因为它同时具备前三种效果。此外,因为 StoreLoad 屏障需要清空写缓冲区、等待所有 pending 的内存操作完成、在多核间同步缓存行等操作,开销最大,须避免过度使用。
内存屏障具有防止编译器和 CPU 将屏障两侧的内存操作随意交换顺序,以及强制将缓冲区(Store Buffer)中的数据刷入缓存或主存,使其他 CPU 核心能“看到”最新的值的功能。在 Java 中,JMM 规定了 volatile、synchronized、final等关键字的语义,JVM 在底层通过插入适当的内存屏障来保证这些语义在多核 CPU 上正确执行。
3.4. volatile
- JMM 对 volatile 的语义要求:
- 可见性:一个线程对 volatile 变量的写,对其他线程立即可见。
- 有序性:volatile 写之前的操作不能重排到写之后;volatile 读之后的操作不能重排到读之前;volatile 写与 volatile 读之间有 happens-before 关系。
- JVM 如何通过内存屏障实现 volatile 语义
| 操作 | 插入的屏障类型 | 目的 |
|---|---|---|
| volatile 写 | StoreStore + StoreLoad | 禁止写前重排序 + 刷新缓存 + 防止写后重排序 |
| volatile 读 | LoadLoad + LoadStore | 禁止读后重排序 + 确保读取最新值 |
3.5. synchronized
- JMM 对 synchronized 的要求
- 进入 synchronized 块(acquire):具有 acquire 语义;
- 退出 synchronized 块(release):具有 release 语义;
- 保证临界区内的操作不会与外部重排序。
- JVM 的实现方式
synchronized 底层使用 monitorenter / monitorexit字节码,JVM 会根据锁状态(偏向锁、轻量级锁、重量级锁)优化。但无论是哪种锁,在获取和释放锁的时候,都会插入内存屏障。
| 操作 | 插入的屏障类型 | 目的 |
|---|---|---|
| 释放锁 | StoreStore + StoreLoad | 确保临界区/写操作前的修改对其他线程可见 |
| 获取锁 | LoadLoad + LoadStore | 确保后续操作能看到之前由 release 操作发布的数据 |
尽管 synchronized 和 volatile 在内存语义上都涉及类似的内存屏障语义,即 synchronized 释放锁 ≈ volatile 写,synchronized 获取锁 ≈ volatile 读,即在 happens-before 规则中:
A 线程 unlock 一个 monitor → B 线程 lock 同一个 monitor ⇒ A happens-before B
A 线程 volatile 写 → B 线程 volatile 读同一个变量 ⇒ A happens-before B
但它们在功能、性能、使用场景和底层实现上存在本质区别:
| 特性 | volatile | synchronized |
|---|---|---|
| 同步范围 | 仅针对单个变量 | 针对整个临界区 |
| 原子性 | 不保证复合操作(如 i++)的原子性 | 保证临界区内所有操作的原子性 |
| 阻塞机制 | 无阻塞,纯内存语义 | 支持线程阻塞与唤醒 |
| 字节码 | 无特殊指令,仅标记 ACC_VOLATILE | 使用 monitorenter / monitorexit |
| JVM 内部 | 依赖内存屏障 + 缓存一致性协议 | 依赖 ObjecMonitor(C++ 实现的 monitor 对象) |
3.6. final
- JMM 对 final 的特殊保证
- 在对象构造函数中写入 final 字段,禁止与该对象引用的发布(赋值给其他变量)重排序;
- 其他线程看到该对象引用非空时,一定能看到 final 字段的正确初始化值。
- JVM 的实现方式
在构造函数结束前,JVM 会插入一个 StoreStore 屏障,以确保:所有 final 字段的写入完成,然后才允许将对象引用赋值给其他变量。
4. 其他补充
4.1. 理清 JMM 中主内存、工作内存与 JVM 中堆内存、栈内存之间的关系
JMM 中的主内存对应 JVM 中线程共享的堆和方法区(存储共享变量),工作内存对应线程私有的虚拟机栈(以及底层 CPU 缓存),用于暂存共享变量的副本,二者是抽象模型与实际内存结构的映射关系。
4.2. 理解 happens-beofore 是一种逻辑上的偏序关系,而不是物理上的时间先后关系
happens-before 是一种逻辑上的偏序关系,用于规定操作之间的可见性和顺序约束,不要求实际执行时间先后,即使 A happens-before B,A 在物理时间上仍可能晚于 B 执行。
4.3. 如何利用 volatile 传递普通变量的可见性
public class VolatileVisibilityExample {private static int normalVar = 0; // 普通变量(非 volatile)private static volatile boolean ready = false; // volatile 标志位// 线程 A:写入数据public static void writer() {normalVar = 42; // (1) 写普通变量ready = true; // (2) 写 volatile 变量 → 触发 happens-before}// 线程 B:读取数据public static void reader() {if (ready) { // (3) 读 volatile 变量System.out.println(normalVar); // (4) 能看到 normalVar == 42}}
}