Skip to content
Go back

happens-before规则详解:JMM的8条核心约束

happens-before规则详解:JMM的8条核心约束

一句话结论(30s)

happens-before 是 JMM 里用来回答「线程 A 对变量的修改何时一定能被线程 B 看到」的核心概念,本质是一组「排序约束」——A happens-before B 意味着 A 的结果对 B 可见、且 A 在 B 之前执行(从 B 的视角看)。它的关键设计是 8 条规则 + 传递性,因为单靠任何一条规则都只能管住一个局部场景,只有靠传递性把它们串成链(如「volatile 写前的普通写 → volatile 写 → volatile 读 → volatile 读后的普通读」),才能让普通变量也获得跨线程可见性。最大的误区是:happens-before 规定的是「可见性与有序性」而非「实际物理执行顺序」,只要结果一致,编译器/CPU 仍可重排。

核心原理(2min)

主流程:线程间「写-读」能否保证可见,取决于这对操作之间是否存在 happens-before 链。8 条规则覆盖了 Java 里所有能建立这种关系的手段——同线程内程序顺序、同一个锁的 unlock→lock、volatile 写→读、thread.start()→子线程任意操作、线程终止→join() 返回、interrupt()→检测到中断、构造结束→finalize(),以及传递性。关键机制是:这些规则不规定底层怎么实现、只规定「在什么条件下结果确定」,编译器靠插入内存屏障来兑现;实战中判断线程安全就是找共享变量的写读点、看它们之间有没有 happens-before 链,没有就说明可能读到脏数据、必须加同步。

底层深入(5-10min)

Java内存模型(JMM, Java Memory Model)定义了多线程环境下变量的访问规则。而happens-before就是JMM中最核心的概念——它回答了一个根本问题:在什么条件下,线程A对变量的修改一定能被线程B看到?

为什么需要happens-before

编译器和处理器为了性能会做各种”重排序”:编译器优化、指令级并行、内存系统重排序。如果没有约束,一个线程写变量的顺序和另一个线程读到的顺序可能完全不同。比如:

// 线程A
x = 1;      // 语句1
flag = true; // 语句2

// 线程B
if (flag) {  // 语句3
    int r = x; // 语句4
}

如果没有happens-before约束,线程B在看到flag=true时,x可能还是0——因为编译器可能重排了语句1和语句2的顺序。

想一想:为什么 B 可能看到 flag=true 却 x=0?因为编译器/CPU 可能把 x=1flag=true 重排,B 侧又没有 happens-before 约束,就看不到 x 的更新——可见性被重排打破了。

happens-before就是JMM规定的”排序约束”:如果操作A happens-before 操作B,那么A的结果对B可见,且A在B之前执行(从B的视角看)。

8条核心规则

1. 程序顺序规则(Program Order Rule)

同一个线程中,按照程序代码的顺序,前面的操作 happens-before 后面的操作。

int a = 1;    // A
int b = 2;    // B
// A happens-before B

但这只是”as-if-serial”语义——只要结果跟顺序执行一致,编译器和CPU可以重排序无依赖的语句。如果a和b之间没有数据依赖关系,JIT可能把b的赋值排到a前面执行,但从结果上看a happens-before b。

2. 监视器锁规则(Monitor Lock Rule)

对于同一个锁,解锁(unlock) happens-before 后面的加锁(lock)

synchronized (lock) {
    sharedVar = 1;  // 在锁内修改
}  // ← unlock在这里

// ... 另一个线程 ...
synchronized (lock) {  // ← lock在这里,能看到sharedVar=1
    int r = sharedVar;
}

这是synchronized保证可见性的理论基础。线程释放锁时,会把修改刷新到主内存;线程获取锁时,会使本地缓存失效重新从主内存读取。

想一想:为什么”解锁后再加锁”能看到之前的修改?因为释放锁时把修改刷新到主内存、获取锁时使本地缓存失效重读,这一”刷一读”就是监视器锁规则在硬件层面的兑现。

3. volatile规则(Volatile Variable Rule)

对一个volatile变量的,happens-before 后续对这个volatile变量的

volatile boolean flag = false;

// 线程A
sharedVar = 42;   // 普通写
flag = true;       // volatile写

// 线程B
if (flag) {        // volatile读
    int r = sharedVar;  // 能看到sharedVar=42
}

volatile写之前的所有操作,对volatile读之后的所有操作可见。这就是”volatile的内存屏障效果”——禁止volatile写与之前的操作重排,禁止volatile读与之后的操作重排。

想一想:为什么 volatile 写前的”普通写”也能被看到?因为 volatile 禁止写与前序操作重排、读与后序操作重排,再靠传递性把”普通写 → volatile 写 → volatile 读 → 普通读”串成一条链,普通变量就获得了跨线程可见性。

4. 线程启动规则(Thread Start Rule)

主线程调用thread.start() happens-before 子线程中的任何操作。

sharedVar = 100;
thread.start();  // happens-before 子线程run()的第一条语句
// 子线程中一定能看到sharedVar=100

5. 线程终止规则(Thread Termination Rule)

线程中的所有操作 happens-before 另一个线程检测到该线程已经终止(thread.join()返回或thread.isAlive()==false)。

// 线程A中
result = compute();
// 线程结束

// 主线程
thread.join();   // 返回后,一定能看到result的值

6. 线程中断规则(Thread Interruption Rule)

对线程的interrupt()调用 happens-before 被中断线程检测到中断事件(通过InterruptedExceptionisInterrupted())。

// 线程A
threadB.interrupt();  // happens-before

// 线程B
if (Thread.interrupted()) {
    // 能看到中断前线程A的所有修改
}

7. 对象终结规则(Finalizer Rule)

对象的构造函数执行结束 happens-before 它的finalize()方法的开始。

这保证了finalize方法能看到构造函数中初始化的值。但再次提醒:finalize()在JDK 9已被废弃,应使用Cleanertry-with-resources替代。

8. 传递性(Transitivity)

如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

这个规则让happens-before形成偏序关系(partial order),可以将多条规则串联起来推导可见性。例如结合规则1、3、8可以推导出:volatile写前的普通写 → volatile写 → volatile读 → volatile读后的普通读,从而让普通变量的修改也能跨线程可见(这就是volatile的”附带可见性”)。

happens-before 不等于”实际执行顺序”

一个常见误区:认为happens-before规定了指令必须按什么顺序执行。实际上:

想一想:为什么有 happens-before 关系还能重排?因为 happens-before 只约束”可见性与有序性”的结果,只要最终结果一致,编译器和 CPU 仍可重排,只是要插内存屏障把可见性保住——它管的是”语义”而非”物理顺序”。

实战:用happens-before推导线程安全性

分析任何并发代码的线程安全性,标准流程是:

  1. 找出所有共享变量的读写点。
  2. 判断每对”写-读”之间是否存在happens-before链。
  3. 不存在 → 可能读到脏数据 → 需要加同步措施(synchronized/volatile/java.util.concurrent工具)。

happens-before是JMM的”法律条文”——它不告诉你具体怎么实现,但告诉你在什么条件下才能得到确定的结果。理解了这8条规则,你就掌握了Java并发的”法理基础”。

章末提问

追问 1:happens-before 到底保证什么?它是不是规定了执行顺序?

回答思路:结论先行——它保证的是”可见性与有序性”,不是实际物理执行顺序。因为即使 A happens-before B,只要结果一致编译器和 CPU 仍可重排,只是会插内存屏障保证 B 能看到 A 的结果;两个没有 happens-before 关系的操作,JVM 可以任意重排。

追问 2:怎么用 happens-before 判断一段并发代码是否线程安全?

回答思路:结论先行——三步走:找出所有共享变量的读写点,判断每对”写-读”之间有没有 happens-before 链,没有就说明可能读到脏数据、必须加同步。因为 8 条规则 + 传递性覆盖了 Java 所有能建立可见性的手段,任何一条链断了,跨线程可见性就无法保证。

追问 3:volatile 为什么能让它之前的普通写也”可见”?

回答思路:结论先行——靠 volatile 的写读屏障 + 传递性串成链。因为 volatile 写禁止与之前操作重排、volatile 读禁止与之后操作重排,于是”普通写 → volatile 写 → volatile 读 → 普通读”满足传递性,普通变量的修改也能跨线程可见,这就是 volatile 的”附带可见性”。


Share this post on:

Previous Post
synchronized锁升级机制
Next Post
OOM类型全景排查——从heap到direct memory的四种溢出路径