08.1 - JMM 与 happens-before
定位: JMM 是 Java 并发编程的"宪法"——定义了线程间共享变量可见性的规则。happens-before 是 JMM 的核心表达,8 条规则划定了什么时候一个线程的写对另一个线程可见。 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 解释 JMM 存在的必要性、背诵 8 条 happens-before 规则、判断特定代码是否具有 happens-before 关系、多线程可见性代码分析
一、这是什么?为什么需要它?
硬件现实:为什么没有 JMM 程序会崩溃
现代 CPU 不是按"顺序执行、立刻写入内存"的方式工作的。它做了三件破坏直觉的事来追求性能:
场景: Thread A 执行 x=1; flag=true; Thread B 执行 if(flag) print(x);
你的直觉: A 先写 x=1, 再写 flag=true → B 读到 flag=true → 一定看到 x=1
硬件实际可能发生的:
┌───── Time ──────────────────────────────────────────────────────┐
│ │
│ Thread A: │
│ x = 1; → A 的 Store Buffer [x=1] (未刷入缓存!) │
│ flag = true; → A 的 Store Buffer [x=1, flag=true] │
│ │
│ Thread B: │
│ if (flag) → B 从主存读到 flag=true (A 的 Store Buffer │
│ 可能在 StoreLoad 前已被部分刷出!) │
│ print(x); → B 从主存读 x → 可能读到 0 而不是 1! │
│ (A 的 Store Buffer 中的 x=1 还没刷入缓存) │
└─────────────────────────────────────────────────────────────────┘
Rex: x=0, flag=true → 程序以为 x 被设置了, 但实际没有!三个破坏直觉的硬件优化:
| 优化 | 做了什么 | 后果 |
|---|---|---|
| CPU Cache (L1/L2) | 每个核心有自己的缓存。读写先走缓存,不是直接走主存 | Thread A 写 x=1 只进了 L1,Thread B 从 L1 读到的是旧值 |
| Store Buffer | 写操作先进 Store Buffer(异步),批量再刷入缓存 | A 的写可能延迟几十甚至几百个 CPU 周期才被其他核心看到 |
| 指令重排序 | CPU + 编译器重排独立指令顺序,最大化流水线利用率 | A 的 flag=true 可能在 x=1 之前就被 B 观察到了 |
JMM:在混乱中建立秩序
JMM 不是物理内存层,而是一组抽象规则。它的定位:
┌──────────────────────────────────────────────────────────────────┐
│ JMM 的定位 │
│ │
│ Java 代码 JMM 抽象层 硬件实现 │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ volatile x │ │ happens-before │ │ x86 TSO │ │
│ │ synchronized│────→│ JMM 规则 │────→│ ARM 弱内存 │ │
│ │ final │ │ 内存屏障插入 │ │ 编译器优化 │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ 程序员依赖的"契约" JVM/CPU 实际执行 │
│ ────────────────── ───────────────── │
│ 只要遵守 JMM 规则, 根据 JMM 要求插入 │
│ 多线程程序就正确 内存屏障保证契约成立 │
└──────────────────────────────────────────────────────────────────┘JMM 的核心契约:
- 可见性:Thread A 的写,什么时候 Thread B 一定能看到?
- 有序性:哪些重排序是允许的,哪些被禁止?
- 原子性:哪些操作是原子的(JMM 保证对齐的 8 字节读写原子)?
"JMM 存在的意义不是防止重排序,而是定义哪些重排序是不安全的,并让程序员可预期。"
二、原理拆解
2.1 JMM 的两大核心领域
JMM 的定义围绕两个维度展开:
线程间交互的两个维度:
┌──────────────────────────────────────────────────────────────┐
│ │
│ 维度一: 主存 vs 工作内存 (抽象概念, 不是硬件映射) │
│ ───────────────────────────────────────────────── │
│ │
│ 从 JMM 抽象角度看, 每个线程有"工作内存" (寄存器+缓存抽象): │
│ │
│ ┌───────────────┐ │
│ │ Thread A │ │
│ │ ┌───────────┐ │ │
│ │ │ 工作内存 │ │──→ 读: 从主存 copy 到工作内存 │
│ │ │ (缓存副本) │ │──→ 写: 从工作内存 flush 到主存 │
│ │ └───────────┘ │ │
│ └───────┬───────┘ │
│ │ 异步 flush / load │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 主存 (Main Memory) │ │
│ │ ┌───────────────┐ │ │
│ │ │ 共享变量 x = 0 │ │ │
│ │ └───────────────┘ │ │
│ └─────────────────────┘ │
│ ▲ │
│ ┌───────┴───────┐ │
│ │ Thread B │ │
│ │ ┌───────────┐ │ │
│ │ │ 工作内存 │ │──→ B 看到的可能还是旧值! │
│ │ │ (缓存副本) │ │ │
│ │ └───────────┘ │ │
│ └───────────────┘ │
│ │
│ 维度二: 重排序 (Reordering) │
│ ──────────────────────────── │
│ │
│ 源码 → 编译器重排序 → CPU 重排序 → 最终执行顺序 │
│ │
│ 允许范围: 不改变单线程执行的 as-if-serial 语义 │
│ 禁止范围: 违反 happens-before 规则的重排序 │
└──────────────────────────────────────────────────────────────┘2.2 happens-before 的本质:不是时间,是可见性保证
这是 happens-before 最常被误解的地方。
❌ 错误理解: "A happens-before B 意味着 A 在时间上先于 B 发生"
✅ 正确理解: "A happens-before B 意味着 A 的可见效果在 B 执行前已经确定"
——可能 A 还没执行完,但 A 的写已经对其他线程可见了。
更精确地说:
if A hb B:
→ A 对共享变量的所有修改, 在 B 开始执行前, 对 B 都可见
→ B 看到的共享变量状态至少和 A 写入后一致 (不会更旧)happens-before 的关键性质:
时间上 A 在 B 之前? 可见性保证 典型场景
┌────────────────┐ ┌────────────────────────┐ ┌────────────────┐
│ A 在 B 之前执行 │ │ A 的写在 B 中一定可见 │ │ unlock hb lock │
│ (时间顺序) │→ │ (部分场景) │ │ 有同步关系 │
└────────────────┘ └────────────────────────┘ └────────────────┘
│ ↑ │
│ 但不一定! │ 关键所在 │
│ 没有同步关系时, │ │
│ A 虽然先执行, │ │
│ B 可能看不到 A 的写│ │
└───────────────────┘ │
▼
System.out.println 输出
"x = 0" — 没有 happens-before!2.3 8 条 happens-before 规则详解
规则 1:程序次序规则 (Program Order Rule)
在一个线程内,按照控制流顺序,前面的操作 happens-before 后面的操作。
int a = 1; // 操作 1
int b = a + 1; // 操作 2 → 可以看到 a=1
int c = b * 2; // 操作 3 → 可以看到 b=2
// 操作 1 hb 操作 2 hb 操作 3
// 但注意: 这不禁止编译器重排不依赖的操作!
int x = 1; // 操作 A
int y = 2; // 操作 B ← 和 A 没有数据依赖, 可以被重排序!
int z = x + y; // 操作 C ← 依赖 A 和 B, 不能重排
// 实际执行顺序: B → A → C 也是合法的 (因为单线程结果不变)WHY: 保证单线程内的基本正确性。如果单线程内的操作都不能保证可见性,一切程序都会崩溃。
规则 2:监视器锁规则 (Monitor Lock Rule)
对一个锁的 unlock happens-before 随后对这个锁的 lock。
Thread A: Thread B:
┌───────────────────────┐ ┌───────────────────────┐
│ synchronized(lock) { │ │ synchronized(lock) { │
│ x = 1; │ │ int r = x; │
│ } // unlock │ │ } // lock │
└───────────────────────┘ └───────────────────────┘
│ │
└──── unlock hb lock ────────────────┘
→ B 能读到 x = 1 (即使 A 没有 volatile!)WHY:这是互斥同步的核心保证。如果加锁后看不到锁保护的变量,synchronized 就失效了。
关键细节:
- "随后"指的是对同一个锁的操作
- lock 和 unlock 必须是同一个 monitor 对象
- B 必须成功获取这个锁,不是随便哪个锁
规则 3:volatile 变量规则 (Volatile Variable Rule)
对一个 volatile 变量的写 happens-before 随后对这个 volatile 变量的读。
volatile boolean flag = false;
int x = 0;
Thread A: Thread B:
┌───────────────────────┐ ┌───────────────────────┐
│ x = 42; │ │ while (!flag); // 读 volatile│
│ flag = true; // volatile写 │ │ int r = x; │
└───────────────────────┘ └───────────────────────┘
│ │
└── volatile写 hb volatile读 ────────┘
→ B 能读到 x = 42!WHY:volatile 的语义就是"写入立即可见"。通过内存屏障禁止重排序 + 保证缓存一致性。
规则 4:传递性 (Transitivity)
如果 A happens-before B,且 B happens-before C,则 A happens-before C。
这是最强大的规则,它把零散的 hb 关系串联成链:
Thread A: Thread B: Thread C:
x = 1; ──hb──→ volatile flag = true; ──hb──→ while(!flag);
int r = x; ← 一定能读到 1!
没有传递性:
x = 1; (无 hb) flag = true; (无 hb) while(!flag); ← 可能读到 x=0!WHY:传递性让 happens-before 形成一个偏序关系。没有传递性,多段独立的同步就串不起来。
规则 5:线程启动规则 (Thread Start Rule)
Thread.start() happens-before 被启动线程中的任何操作。
int x = 0;
Thread t = new Thread(() -> {
System.out.println(x); // 一定能读到 1!
});
x = 1; // 对 x 的修改
t.start(); // start() hb 线程内所有操作WHY:如果启动规则不成立,主线程在 start() 之前准备的上下文(配置、初始化数据)子线程可能看不到——线程间最基本的通信就断了。
规则 6:线程终止规则 (Thread Termination Rule)
线程中的所有操作 happens-before 其他线程从 Thread.join() 成功返回。
Thread t = new Thread(() -> {
result = computeExpensive(); // 复杂计算
});
t.start();
t.join(); // 等待线程结束
// join() 返回后, result 的结果一定可见
System.out.println(result); // 能看到 computeExpensive() 的结果WHY:join() 的语义就是"等待线程执行完毕"。如果 join() 返回后还看不到结果,join 就没有意义了。
规则 7:线程中断规则 (Thread Interruption Rule)
对线程的 interrupt() 调用 happens-before 被中断线程检测到中断事件(通过 interrupted() 或 InterruptedException)。
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 工作循环
}
// 中断检测 happens-after interrupt() 调用
cleanup(); // 能清理所有中断前的状态
});
t.start();
Thread.sleep(1000);
t.interrupt(); // interrupt() hb 线程检测到中断WHY:中断是一种协作机制。如果线程调用 interrupt() 后,目标线程的 isInterrupted() 还返回 false,中断机制就没有意义了。
规则 8:对象终结规则 (Finalizer Rule)
一个对象的构造函数的结束 happens-before 该对象的 finalize() 方法的开始。
class Resource {
FileHandle handle;
Resource(String path) {
handle = openFile(path); // 构造函数初始化
}
@Override
protected void finalize() {
handle.close(); // 一定能看到 handle 被初始化
}
}WHY:保证 GC 调用 finalize() 时,对象的所有 final 字段和非 final 字段(只要在构造函数中设置)都可见。防止 finalize() 读到未初始化的字段。
2.4 happens-before 关系链图解
┌─────────────────────────────────────────────────────────────────────────┐
│ happens-before 关系链:从 A 到 D 的可见性传递 │
│ │
│ Thread A: Thread B: Thread C: │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ x = 1 │ │ │ │ │ │
│ │ lock m │ │ lock m │ │ │ │
│ │ y = 1 │──hb──→│ r1 = y │ │ │ │
│ │ unlock m │ │ unlock m │ │ │ │
│ │ volatile v=T │──hb──→│ │──hb──→ │ r2 = v │ │
│ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ 完整 hb 链: │
│ x=1 ─(程序次序)─→ lock m ─(程序次序)─→ y=1 ─(监视器锁)─→ lock m ─(程序次序)─→ r1=y │
│ x=1 ─(传递性)──→ r1=y (x=1 hb unlock m hb lock m hb r1=y) │
│ v=T ─(volatile)─→ r2=v │
│ │
│ 可见性保证: │
│ ✓ Thread B 中 r1 一定是 1 (因为 unlock hb lock) │
│ ✓ Thread C 中 r2 一定是 true (因为 volatile 写 hb volatile 读) │
│ ✗ Thread B 中 x 不一定可见! 为什么? │
│ → x=1 在 synchronized 块外! unlock 只保证锁内的内容可见 │
│ → 如果 x=1 放在 lock 之前, unlock 不保证 x 的可见性 │
└─────────────────────────────────────────────────────────────────────────┘三、图解全景
3.1 没有 happens-before 的后果
场景: 两个线程, 共享变量 x 和 ready, 无同步
Thread A: Thread B:
┌──────────────────────────┐ ┌────────────────────────────────────┐
│ x = 42; │ │ while (!ready); // 可能永远循环! │
│ ready = true; │ │ System.out.println(x); // 可能输出 0!│
└──────────────────────────┘ └────────────────────────────────────┘
可能观察到的执行顺序 (编译器/CPU 重排序后):
时间 ──────────────────────────────────────────────────────────────→
Thread A: ready = true ──── Store Buffer ──→ x = 42 (还在 Store Buffer!)
Thread B: while(!ready) → false → println(x) → 读到 0!
没有 happens-before 关系 → A 的写对 B 没有可见性保证!3.2 使用同步修复
解决方案 A: volatile
┌───────────────────────────────┐
│ volatile boolean ready = false; │
│ int x; │
│ │
│ Thread A: │ Thread B: │
│ x = 42; │ while (!ready); // volatile 读 │
│ ready = true; // volatile 写 │ println(x); // 一定能看到 42 │
└───────────────────────────────┘
原理: volatile 写 hb volatile 读 → 保证 x=42 在 ready=true 之前可见
解决方案 B: synchronized
┌───────────────────────────────┐
│ Object lock = new Object(); │
│ int x; │
│ │
│ Thread A: │ Thread B: │
│ synchronized(lock) { │ synchronized(lock) { │
│ x = 42; │ println(x); // 一定能看到 42 │
│ } // unlock │ } // lock │
└───────────────────────────────┘
原理: unlock hb lock → 锁内的写入对后续获取同一锁的线程可见
解决方案 C: Thread.join()
┌───────────────────────────────┐
│ Thread t = new Thread(() -> { │
│ x = 42; │
│ }); │
│ t.start(); │
│ t.join(); │
│ println(x); // 一定能看到 42 │
└───────────────────────────────┘
原理: 线程内动作 hb join() 返回四、实战验证
验证 1:无 happens-before 导致可见性问题
public class VisibilityProblem {
static boolean running = true; // 没有 volatile!
public static void main(String[] args) throws Exception {
Thread worker = new Thread(() -> {
long count = 0;
while (running) { // 可能永远看不到主线程的修改!
count++;
}
System.out.println("Worker stopped, count=" + count);
});
worker.start();
Thread.sleep(1000); // 等 worker 启动
running = false; // 修改标志位
System.out.println("Main: set running=false");
worker.join();
}
}可能的现象:
1. 正常: Worker 看到 running=false, 打印后退出 (约 1-2 秒)
2. 可能: Worker 永远看不到 running=false, 程序不退出
3. 某种 JVM 实现下: 编译器把 while(running) 优化为 while(true) (因为 running 不被 "修改")
修复: static volatile boolean running = true;
→ volatile 写 hb volatile 读 → worker 线程一定能看到主线程的修改验证 2:synchronized 保证可见性
public class SynchronizedVisibility {
static int x = 0;
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread writer = new Thread(() -> {
synchronized (lock) {
x = 42;
} // unlock → 刷新 x 到主存
});
Thread reader = new Thread(() -> {
synchronized (lock) { // lock → 从主存读 x
System.out.println("x = " + x);
}
});
writer.start();
writer.join();
reader.start();
reader.join();
// 输出: x = 42 (一定有)
}
}验证 3:happens-before 传递性
public class TransitivityDemo {
static int a = 0;
static volatile int b = 0;
static int c = 0;
public static void main(String[] args) throws Exception {
// Thread 1: a = 1; → (写 volatile) b = 1;
// Thread 2: while(b != 1); → (读 volatile) b → c = a;
// 如果 1 hb 2, 则 c 一定是 1
Thread t1 = new Thread(() -> {
a = 1; // 操作 1
b = 1; // 操作 2: volatile 写
});
Thread t2 = new Thread(() -> {
while (b != 1) { // 操作 3: volatile 读
Thread.yield();
}
c = a; // 操作 4: 非 volatile
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("c = " + c); // 总是 1
// 传递性: a=1 hb volatile写b=1 hb volatile读b hb c=a
}
}五、面试视角
| 追问 | 答案要点 |
|---|---|
| JMM 是什么?为什么需要它? | JMM 是 Java 内存模型的抽象规范,定义了多线程共享变量可见性的规则。需要它是因为:CPU 缓存导致写不立即可见,Store Buffer 延迟刷入缓存,指令重排序改变实际执行顺序。没有 JMM 多线程程序完全不可预期。 |
| happens-before 的 8 条规则? | ①程序次序 ②监视器锁 ③volatile 变量 ④传递性 ⑤线程启动 ⑥线程终止 ⑦线程中断 ⑧对象终结。每条都要能说清楚 + 给出场景。 |
| happens-before 是时间先后关系吗? | 不是。hb 是可见性保证,不是时间顺序。A hb B 意味着 A 的修改对 B 可见,不保证 A 在时间上先于 B 执行。可能 A 的 Store Buffer 还没刷完,但通过 hb 链 B 已经能感知到结果。 |
| 没有 happens-before 关系的代码一定会出问题吗? | 不一定。x86 TSO 模型本身很强,很多重排序在 x86 上不会实际发生。但程序不能依赖这个——弱内存模型(ARM、RISC-V)上必然会失败。靠"我的机器上跑没问题"做并发保证是危险的。 |
| volatile 写和 synchronized 的可见性保证有什么区别? | volatile 写 hb volatile 读:只保证对单个 volatile 变量的可见性。synchronized:lock/unlock 之间所有共享变量的修改都可见于随后获取同一锁的线程。volatile 更轻量但保证范围更小。 |
| 用 synchronized 修饰两个方法可以建立 hb 关系吗? | 可以,但前提是使用的是同一个 monitor 对象。两个线程各自进入同一个对象的 synchronized 方法 = 使用同一把锁 → unlock hb lock 成立。但如果两个方法是不同对象的 synchronized 方法,则没有 hb 关系。 |
| JMM 中的"工作内存"是硬件上的什么? | 不是精确的硬件映射,而是抽象概念。它代表了线程能访问的所有寄存器+缓存层级(寄存器、L1、L2、Store Buffer 等)。不同硬件架构上,"工作内存"对应的物理存储不同。 |
| happens-before 和 as-if-serial 的关系? | as-if-serial:单线程内重排序不改变执行结果(编译器和 CPU 都能重排)。happens-before:多线程间的可见性保证。as-if-serial 是 happens-before 程序次序规则的基础——如果单线程重排序违反 as-if-serial,程序次序规则也不成立。 |
📚 相关链接
- **volatile的内存语义** — volatile 的 happens-before 实现机制(内存屏障)
- **final的内存语义** — final 字段的 JMM 特殊处理
- **synchronized的JVM实现** — 监视器锁规则的底层实现
- **对象内存布局(MarkWord与Klass与实例数据)** — MarkWord 是锁信息的载体
- ← 返回 **JMM索引**