0.引言
理解 JMM (Java Memory Model - JMM) 是掌握 Java 并发编程的关键,它定义了多线程环境下,线程如何与主内存以及彼此之间交互内存数据。
核心目标: JMM 旨在解决多线程编程中的三个核心问题:
- 原子性 (Atomicity): 一个操作是不可中断的,要么全部执行成功,要么完全不执行。
- 可见性 (Visibility): 一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
- 有序性 (Ordering): 程序执行的顺序可能和代码编写的顺序不一致(指令重排序),JMM 定义了在何种情况下这种重排序是被允许或禁止的。
1.第一层:浅显易懂 - 为什么需要 JMM? (The Problem)
想象一下一个简单的场景:
public class VisibilityProblem {private static boolean flag = false; // 共享变量private static int value = 0; // 共享变量public static void main(String[] args) {// 线程 Anew Thread(() -> {while (!flag) { // 循环检查 flag// 空循环,等待 flag 变为 true}System.out.println("Value: " + value); // 打印 value}).start();// 线程 Bnew Thread(() -> {value = 42; // 步骤 1:设置 valueflag = true; // 步骤 2:设置 flag}).start();}
}
- 直觉期望: 线程 B 先设置
value = 42
,然后设置flag = true
。线程 A 看到flag
变为true
后跳出循环,打印出Value: 42
。 - 现实问题:
- 可见性问题: 线程 B 修改了
flag
和value
,但这些修改可能只存在于线程 B 的 CPU 缓存或寄存器中,没有立即写回主内存。线程 A 在自己的缓存中看到的flag
可能仍然是false
,导致它永远无法跳出循环。即使跳出了循环,它看到的value
也可能是0
而不是42
。 - 有序性问题: 编译器或处理器为了优化性能,可能会对指令进行重排序。线程 B 中的
flag = true
操作可能在value = 42
之前执行。如果此时线程 A 看到了flag
为true
而跳出循环,它看到的value
就可能是未初始化的0
。
- 可见性问题: 线程 B 修改了
JMM 就是为了解决这类在多线程环境下因缓存、指令重排序等优化带来的不可预测行为而制定的规则。
2.第二层:核心概念 - JMM 的抽象模型 (The Abstraction)
JMM 定义了一个抽象的内存模型,它屏蔽了底层硬件的具体实现细节(如 CPU 缓存、缓存一致性协议),为 Java 程序员提供了一个统一的视图:
- 主内存 (Main Memory):
- 存储所有共享变量(实例字段、静态字段、数组元素)的原始值。
- 是线程间共享的区域。
- 工作内存 (Working Memory / Local Memory):
- 每个线程都有自己的工作内存。
- 存储该线程使用到的共享变量的副本。
- 线程对共享变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存。
- 工作内存是 JMM 的一个抽象概念,它涵盖了 CPU 寄存器、各级缓存、写缓冲区等硬件优化。
内存交互操作 (8 种原子操作)
JMM 定义了 8 种原子操作来描述线程、工作内存和主内存之间的交互:
lock
(锁定):作用于主内存变量,标识其为线程独占状态。unlock
(解锁):作用于主内存变量,释放锁定状态。read
(读取):作用于主内存变量,将变量值从主内存传输到线程的工作内存。load
(载入):作用于工作内存变量,将read
操作得到的值放入工作内存的变量副本中。use
(使用):作用于工作内存变量,将变量值传递给执行引擎(如进行计算)。assign
(赋值):作用于工作内存变量,将执行引擎接收到的值赋给工作内存变量。store
(存储):作用于工作内存变量,将变量值从工作内存传输到主内存。write
(写入):作用于主内存变量,将store
操作传输过来的值放入主内存变量中。
规则: JMM 规定这些操作必须满足特定的顺序和约束(如 read
和 load
、store
和 write
必须成对按顺序出现),但允许在成对操作之间插入其他操作(这是导致可见性和有序性问题的根源之一)。更关键的规则体现在 happens-before
原则上。
3.第三层:关键机制 - Happens-Before (HB)
happens-before
是 JMM 的核心概念,它定义了两个操作之间的偏序关系。如果操作 A happens-before
操作 B,那么:
- 可见性保证: A 对共享变量的修改(结果)一定对 B 可见。
- 有序性保证: A 在程序顺序上一定排在 B 之前执行(禁止某些重排序)。
注意: happens-before
并不一定意味着时间上的先后!它强调的是可见性和顺序的保证。如果两个操作之间没有 happens-before
关系,JVM 可以随意对它们进行重排序。
3.1JMM 定义的天然 Happens-Before 规则
- 程序次序规则 (Program Order Rule): 在同一个线程中,按照控制流顺序(可能是分支、循环等),前面的操作
happens-before
后面的操作。 - 管程锁定规则 (Monitor Lock Rule): 一个
unlock
操作happens-before
于后续对同一个锁的lock
操作。 - volatile 变量规则 (Volatile Variable Rule): 对一个
volatile
变量的写操作happens-before
于后续对这个变量的读操作。 - 线程启动规则 (Thread Start Rule):
Thread.start()
调用happens-before
于被启动线程中的任何操作。 - 线程终止规则 (Thread Termination Rule): 线程中的所有操作都
happens-before
于其他线程检测到该线程已经终止(如Thread.join()
返回成功或Thread.isAlive()
返回false
)。 - 线程中断规则 (Thread Interruption Rule): 对线程
interrupt()
的调用happens-before
于被中断线程检测到中断事件(抛出InterruptedException
或调用isInterrupted()
/interrupted()
)。 - 对象终结规则 (Finalizer Rule): 一个对象的初始化完成(构造函数执行结束)
happens-before
于它的finalize()
方法的开始。 - 传递性 (Transitivity): 如果 A
happens-before
B,且 Bhappens-before
C,那么 Ahappens-before
C。
happens-before
的意义: 程序员只需要利用这些规则(主要是通过 synchronized
、volatile
、final
等关键字以及 java.util.concurrent
包中的工具),就能确保多线程操作的可见性和有序性,无需关心底层复杂的缓存和重排序细节。
4.第四层:深入剖析 - volatile 关键字
volatile
是 JMM 中最重要的关键字之一,它提供了比 synchronized
更轻量级的同步机制。
4.1volatile 的语义
- 保证可见性:
- 对一个
volatile
变量的写操作,会立即刷新到主内存。 - 对一个
volatile
变量的读操作,会从主内存中读取最新的值(或保证看到最近写入的值)。 - 这通过禁止编译器/处理器对
volatile
变量的读写操作进行缓存优化来实现。
- 对一个
- 禁止指令重排序 (部分):
- 编译器在生成字节码时,会在
volatile
写操作前后插入写屏障 (StoreStore + StoreLoad)。 - 在
volatile
读操作前后插入读屏障 (LoadLoad + LoadStore)。 - 写屏障 (Store Barrier):
StoreStore
: 确保在该屏障之前的所有普通写操作都刷新到主内存(对其他线程可见)。StoreLoad
: 确保在该屏障之前的volatile
写操作都完成,并且刷新到主内存后,才能执行该屏障之后的volatile
读/写操作(开销较大,通常由StoreLoad
承担)。
- 读屏障 (Load Barrier):
LoadLoad
: 确保在该屏障之后的所有读操作(普通读或volatile
读)都在该屏障之后的读操作之前执行(禁止重排序),并且强制从主内存或最新缓存中加载数据。LoadStore
: 确保在该屏障之后的所有写操作都在该屏障之后的写操作之前执行(禁止重排序),并且这些写操作的数据依赖在该屏障之前的读操作已完成。
- 这些屏障共同作用,确保了
volatile
变量读写操作相对于其前后代码的相对顺序,从而实现了happens-before
规则中的有序性保证。
- 编译器在生成字节码时,会在
4.2volatile 的局限性
- 不保证原子性:
volatile
不能保证复合操作的原子性。例如volatile int count; count++;
这个操作 (count++
包含读-改-写三步) 在多线程下仍然是不安全的。需要使用synchronized
或AtomicInteger
等。
4.3volatile 的典型用法
- 状态标志位: 如开头的例子,用
volatile boolean flag;
来安全地通知其他线程状态改变。 - 一次性安全发布 (One-Time Safe Publication): 利用
volatile
写操作的StoreStore
屏障,确保在发布对象引用之前,对象的初始化已经完全完成(构造函数结束)。
如果没有public class Singleton {private static volatile Singleton instance; // volatile 保证安全发布private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次检查 (无锁)synchronized (Singleton.class) {if (instance == null) { // 第二次检查 (加锁)instance = new Singleton(); // volatile 写}}}return instance;} }
volatile
,其他线程可能看到一个未初始化完成的Singleton
对象(指令重排序导致引用赋值在构造函数完成之前发生)。 - 独立观察 (Independent Observation): 定期发布观察结果供其他程序使用。
- 开销较低的读-写锁策略: 当读远多于写时,可以用
volatile
保证写操作的可见性,读操作不需要加锁(但写操作需要额外的同步机制如synchronized
或 CAS 来保证原子性)。
5.第五层:JMM 与并发编程实践
理解 JMM 对于编写正确、高效、可预测的并发程序至关重要:
- 优先使用高级并发工具:
java.util.concurrent
包 (ConcurrentHashMap
,ExecutorService
,CountDownLatch
,CyclicBarrier
,AtomicXxx
类等) 是构建在 JMM 基础之上的,它们通常比直接使用synchronized
和volatile
更安全、更高效、更易用。理解 JMM 能让你更好地理解和使用这些工具。 - 正确使用 synchronized:
synchronized
块不仅提供互斥(原子性),也提供强大的内存语义:进入synchronized
块(获得锁)相当于执行一个volatile
读操作(能看到之前持有该锁的线程的所有修改),退出synchronized
块(释放锁)相当于执行一个volatile
写操作(将修改刷新到主内存)。这确保了临界区内外操作的可见性和有序性。 - 理解 final 字段: 被
final
修饰的字段在构造函数中初始化后,其值对其他线程是可见的(无需同步),前提是对象的引用本身是正确发布的(例如通过volatile
或synchronized
安全发布规则)。这是 JMM 对final
的特殊保证。 - 避免过度同步: 不必要的同步会带来性能开销。理解 JMM 可以帮助你判断何时需要同步(主要是为了保护共享可变状态),何时可以避免。
- 警惕内存可见性导致的微妙 Bug: 很多并发 Bug 不是由竞态条件引起的,而是由内存可见性问题引起的。理解 JMM 是诊断这类 Bug 的基础。
6.总结
- JMM 是什么? 一个规范,定义了多线程环境下 Java 程序如何与内存交互,确保程序在并发执行时的可见性、有序性(以及通过其他机制如锁保证的原子性)。
- 核心问题: 解决因 CPU 缓存、指令重排序导致的可见性和有序性问题。
- 抽象模型: 主内存(共享)和工作内存(线程私有副本),定义了 8 种内存交互操作。
- 核心机制:
happens-before
关系。它定义了操作间的可见性和顺序保证。JMM 定义了一系列天然规则(程序次序、锁、volatile
、线程启动/终止等)。 volatile
关键字:- 保证可见性(写立即刷新,读获取最新值)。
- 通过内存屏障(
StoreStore
,StoreLoad
,LoadLoad
,LoadStore
)禁止特定类型的指令重排序。 - 不保证原子性。
- 实践意义: 理解 JMM 是编写正确、高效并发 Java 程序的基础。它解释了高级并发工具的工作原理,指导你正确使用
synchronized
、volatile
、final
,并帮助你诊断复杂的并发 Bug。
掌握 JMM 需要时间和实践。从理解基本问题和抽象模型开始,逐步深入到 happens-before
和 volatile
的细节,最终将其应用于并发编程实践中,是学习 JMM 的有效路径。