← 返回题目列表

happens-before 原则是什么?指令重排序和内存屏障是怎么回事?

高频 困难 第 18 / 31 题 更新于 2026/07/26
happens-beforeJMM指令重排序内存屏障

简化版

happens-before 是 JMM(Java 内存模型)用来定义「一个操作的结果对另一个操作是否可见」的规则。如果操作 A happens-before 操作 B,那么 A 的结果(对内存的修改)一定对 B 可见,且 A 的执行顺序看起来在 B 之前。它是「可见性 + 有序性」的判断依据。之所以需要它,是因为 CPU 和编译器为了性能会指令重排序,可能打乱代码顺序;happens-before 规定了哪些顺序不许被重排、必须保证可见,底层靠内存屏障实现。常见规则有:程序顺序、锁、volatile、传递性、线程启动/终止等。

详细版

为什么需要 happens-before:为提速,编译器、CPU 会重排指令,各级缓存也让一个线程的写不一定立刻被另一个线程看到。如果没有规则约束,多线程下你根本无法推断「我写的值别的线程能不能看到、以什么顺序看到」。happens-before 就是 JMM 给程序员的「可见性保证契约」——只要符合某条规则,就不用关心底层怎么重排,可见性一定成立。

八条核心规则(记住常考的前 5 条):

  1. 程序顺序规则:单线程内,前面的操作 happens-before 后面的操作(单线程内看起来是顺序的)。
  2. 锁规则:对一个锁的 unlock happens-before 后续对同一个锁的 lock
  3. volatile 规则:对 volatile 变量的写 happens-before 后续对它的读。
  4. 传递性:A hb B,B hb C,则 A hb C。
  5. 线程启动规则Thread.start() happens-before 该线程内的任何操作。
  6. 线程终止规则:线程内所有操作 happens-before 其他线程检测到它终止(join 返回、isAlive 为 false)。
  7. 中断规则interrupt() happens-before 被中断线程检测到中断。
  8. 对象终结规则:对象构造完成 happens-before finalize() 开始。

指令重排序的三个来源:编译器优化重排、CPU 指令级并行重排、内存系统重排(缓存)。重排的底线是 as-if-serial:单线程内重排后的结果必须和顺序执行一致(有数据依赖的不重排),但多线程下 as-if-serial 不保证跨线程可见性,这才需要 happens-before。

// 经典重排问题:双重检查锁(DCL)必须给 instance 加 volatile
class Singleton {
    private static volatile Singleton instance;   // 少了 volatile 就有 bug
    static Singleton get() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();  // 这行可能被重排
            }
        }
        return instance;
    }
}

⚠️ new Singleton() 不是原子操作,分「分配内存→初始化对象→把引用赋给 instance」三步。若无 volatile,2、3 可能被重排成「先赋引用、后初始化」,别的线程会拿到一个「非 null 但没初始化完」的半成品对象。volatile 禁止这种重排。

完整版教学

一、问题的根源:可见性与有序性

多线程 bug 大多来自两个反直觉的现象:

可见性问题:线程 A 改了共享变量,线程 B 却看不到(各自缓存、写没刷回主存)
有序性问题:代码写的是 a=1; b=2,实际执行可能是 b=2; a=1(被重排了)

单线程时这俩都不是问题——CPU 保证单线程结果正确(as-if-serial)。但多线程下,一个线程看另一个线程的操作,顺序可能是乱的、值可能是旧的。JMM 不可能禁止所有重排和缓存(那样性能太差),于是它换了个思路:不管底层怎么优化,只要你的代码符合某条 happens-before 规则,我就保证可见性和有序性。 happens-before 是程序员和 JVM 之间的契约,让你不必理解 CPU 细节也能写对并发代码。

二、happens-before 到底在说什么

注意一个常见误解:happens-before 不是「时间上先发生」,而是「前者的结果对后者可见,且顺序不被感知为颠倒」

「A happens-before B」意味着:
  ① A 对内存的所有修改,B 一定能看到(可见性)
  ② 在 B 看来,A 的操作发生在 B 之前(有序性)
它不要求 A 在物理时间上一定早于 B,只要求"结果可见 + 顺序不乱"

举例:volatile boolean flag 的写 happens-before 后续的读。所以线程 A flag=true 之前写的所有普通变量,线程 B 读到 flag==true 后也都能看到——哪怕那些普通变量不是 volatile。这就是 volatile 的「附带可见性」,是很多并发工具的基石。

三、程序顺序 vs as-if-serial:单线程的假象

「程序顺序规则」说单线程内前面 hb 后面,但这不等于不重排。真相是:

as-if-serial:单线程内,无论怎么重排,最终结果必须和顺序执行一致
              → 有数据依赖的不能重排,无依赖的可以随便排
int a = 1;    // ①
int b = 2;    // ②  ①②无依赖,可能被排成 ②①,但你察觉不到
int c = a + b;// ③  ③依赖①②,不会排到它们前面

单线程看起来「顺序执行」只是 as-if-serial 制造的假象——底层可能已经重排,只是结果一致所以你看不出来。而这个假象只对单线程成立:多线程时,线程 A 内部的重排(对 A 自己无害)可能让线程 B 观察到诡异的顺序。DCL 的 bug 正是如此:instance = new Singleton() 内部重排对创建线程无害,却让别的线程看到半成品。

四、指令重排的三层来源与内存屏障

重排来自三个层次,happens-before 靠内存屏障(Memory Barrier) 在需要处禁止它:

重排来源:
  1. 编译器:调整指令顺序做优化
  2. CPU:指令级并行、乱序执行
  3. 缓存/写缓冲:写不立即刷主存,读不立即读主存

内存屏障:一种 CPU 指令,插在特定位置,禁止跨屏障重排 + 强制刷缓存
  LoadLoad   :屏障前的读 先于 屏障后的读
  StoreStore :屏障前的写 先于 屏障后的写
  LoadStore / StoreLoad :混合方向

volatile 的实现就是插屏障:volatile 写之后插 StoreStore+StoreLoad(保证写对别人可见、且不与后续操作重排),volatile 读之前插 LoadLoad+LoadStore。synchronized 则在 unlock 时把工作内存刷回主存、lock 时从主存重读。程序员不用直接写屏障,JMM 通过 happens-before 规则替你在合适位置插入。

五、锁规则与 volatile 规则:最常用的两条

实战中最常用的可见性保证就这两条:

锁规则:unlock(M) happens-before 后续 lock(M)
  → synchronized 块里的修改,下一个进入同步块的线程一定看得到
  synchronized(lock) { count++; }   // A 线程改
  synchronized(lock) { read count; }// B 线程读,一定看到 A 的修改

volatile 规则:write(v) happens-before 后续 read(v)
  volatile boolean ready; int data;
  线程A: data = 42; ready = true;   // ready 写 hb ready 读
  线程B: if (ready) use(data);      // 读到 ready==true 时,data=42 一定可见

注意 volatile 的「附带效应」:它不仅保证自己可见,还保证它之前的普通写对「读到该 volatile 的线程」可见(靠屏障 + 传递性)。这就是为什么 ready 加 volatile 后,普通变量 data 也安全了。

六、传递性把规则串起来

传递性(A hb B、B hb C ⇒ A hb C)是把单条规则组合成完整保证的黏合剂。回到上面的例子完整推一遍:

线程A: data = 42;      (①)
       ready = true;   (②)   volatile 写
线程B: while(!ready);  (③)   volatile 读,读到 true
       use(data);      (④)

① hb ②:程序顺序规则(单线程内)
② hb ③:volatile 规则(写 hb 后续读)
③ hb ④:程序顺序规则
传递性:① hb ④ → 线程B 的 use(data) 一定看到 data=42 ✓

正是靠传递性,一个 volatile 变量的可见性能「扩散」到它前面的所有普通变量。CountDownLatch、线程池、Future 等能安全传递结果,底层都依赖这种 happens-before 链条。

记忆钩子:「happens-before = 可见 + 有序的契约,不是时间先后」;最常用三条——程序顺序(单线程)、锁(unlock hb lock)、volatile(写 hb 读),再加传递性把它们串成链。

七、常见误区与追问

  • 误区:happens-before 表示 A 在时间上先于 B 执行。 它表示 A 的结果对 B 可见、顺序不被感知为颠倒,与物理时间先后无关。
  • 误区:单线程内代码不会被重排。 会重排,只是 as-if-serial 保证结果和顺序执行一致,你察觉不到;多线程下这个假象会破。
  • 误区:volatile 只保证单个变量可见。 它还有屏障+传递效应——volatile 写之前的普通变量修改,对读到该 volatile 的线程也可见。
  • 误区:加了 synchronized 还要担心重排。 同步块内外由锁规则保证可见性和有序性,正确加锁的代码不需要额外关心重排。
  • 追问:as-if-serial 和 happens-before 什么关系? as-if-serial 是单线程内的重排底线(结果一致);happens-before 是跨线程的可见性/有序性规则,多线程正确性靠后者。
  • 追问:DCL 单例为什么必须 volatile? new 分三步(分配、初始化、赋引用),无 volatile 时初始化和赋引用可能重排,别的线程会拿到未初始化完的半成品对象;volatile 禁止该重排。
  • 追问:内存屏障是什么?和 happens-before 什么关系? 内存屏障是 CPU 指令,禁止跨屏障重排并强制刷缓存;happens-before 是抽象规则,JVM 用插入内存屏障来实现这些规则。

八、加强记忆

happens-before 是 JMM 给程序员的「可见性 + 有序性契约」——记住它说的不是「时间上谁先」,而是「A 的修改对 B 可见、且 B 感知不到顺序颠倒」。它之所以存在,是因为编译器、CPU、缓存三层都会重排指令来提速,单线程靠 as-if-serial(有依赖不排、结果一致)维持「顺序执行」的假象,但这假象跨线程就破了,所以需要一套跨线程规则。最常用的三条是程序顺序(单线程前 hb 后)、锁(unlock hb 同锁 lock)、volatile(写 hb 后续读),再用传递性串成链,让一个 volatile 变量的可见性扩散到它前面的普通写。底层实现靠内存屏障禁止跨屏障重排并刷缓存。经典应用是 DCL 单例——new 分三步可能重排,必须给字段加 volatile 防止别人拿到半成品。一句话「重排为了快,happens-before 保证可见有序,锁和 volatile 是两大主力,传递性把它们连成保证链」。