Skip to content

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 导致可见性问题

java
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 保证可见性

java
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 传递性

java
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索引**

Knowledge4J — Java 知识库