什么是 happens-before 原则?它约束了什么?

#Java#面试八股

什么是 happens-before 原则?它约束了什么?

结论先行:happens-before(先行发生)是 Java 内存模型给出的偏序关系:如果 A happens-before B,那么 A 的执行结果对 B 一定可见,并且 A 不会被重排序到 B 之后。它是判断"多线程下我能不能看到别人写的值"的唯一标准,不是时间上的先后。

1. 八条规则

规则表述例子
程序顺序同一线程内,前面的操作 hb 后面的操作a = 1; hb b = 2;
监视器锁解锁 hb 后续对同一锁的加锁synchronized 块结束 → 下一个进入的线程
volatilevolatile 写 hb 后续对该变量的读flag = true hb while (!flag)
线程启动start() hb 新线程中的所有操作启动前赋的值,新线程一定看得到
线程终止线程内所有操作 hb 其他线程的 join() 返回join() 之后能读到子线程的结果
线程中断interrupt() hb 被中断线程检测到中断isInterrupted() 返回 true 时能看到中断前的写入
对象终结构造函数完成 hb finalize() 开始对象初始化对终结器可见
传递性A hb B 且 B hb C ⇒ A hb C由前七条组合推导

2. 传递性怎么用

主线程                                 worker 线程
───────                               ────────────
x = 1;                  ┐
flag = true;            ┘ volatile 写 ──hb──▶ 读到 flag == true ──▶ 读 x 必然为 1
                        ↑
                  程序顺序规则:x=1 hb flag=true
                  volatile 规则:flag=true hb 读 flag
                  传递性:x=1 hb 读 x

这就是 volatile 能"顺带发布"普通变量的原因:写 volatile 时,之前的所有写都已经刷出去了。

public class HbDemo {
    private static int x = 0;
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (!flag) { }        // volatile 读:与写建立 hb
            System.out.println(x);   // 必然输出 1
        }).start();

        x = 1;                       // 普通写
        flag = true;                 // volatile 写:把 x=1 一起"发布"
    }
}

反过来,如果去掉 volatile,while (!flag) 可能永远读到 false,x 也可能读到 0——因为两条线程之间没有任何 hb 关系。

3. 三个常见误解

误解一:"happens-before 就是先执行"。 不是。它约束的是可见性与合法重排,不是真实时钟。两个操作即使物理上先发生了,只要没有 hb 关系,结果就不可信。

误解二:"同一个线程内的代码不会被重排"。 会被重排,只是不会影响单线程的执行结果(as-if-serial 语义)。指令重排、编译器优化、CPU 乱序执行都真实存在,happens-before 只保证"有 hb 关系的操作"顺序不乱。

误解三:"只有加锁才有 hb"。 start()、join()、interrupt()、final 字段、CountDownLatch.await()、Future.get()、BlockingQueue.put/take 都能建立 hb。锁只是其中最常见的一种。

4. 常见追问

追问一:final 字段有什么特殊 hb? 构造函数中对 final 字段的写,hb 于"构造完成后把引用赋给其他线程"的读。所以只要对象构造期间 this 没有逃逸,即使没有同步,其他线程看到 final 字段也一定是构造后的值——这是不可变对象能安全发布的基础。

追问二:Thread.start() 之前的写,新线程一定看得到吗? 一定。线程启动规则保证 start() 之前的所有写在子线程中可见,所以"启动前准备好参数"是安全模式。

追问三:join() 为什么能读到子线程的结果? 线程终止规则:子线程中的所有操作 hb 于其他线程从 join() 成功返回。所以 join() 之后读共享变量不需要额外同步。

追问四:happens-before 能推出"不需要 volatile"吗? 有时能。如果两条线程之间已经有锁或 join 建立 hb,那么共享变量的可见性已被保证。但注意:happens-before 保证的是可见性,不是原子性,count++ 依然要加锁。

追问五:两条线程之间没有任何 hb 关系,就一定出错吗? 不一定"必然"出错,但结果是不可预期的:可能在开发机上跑一万次都对,在生产环境换一颗 CPU、换一版 JDK 就错了。并发 bug 的可怕之处正在于此——它不报错,只是偶尔给出错误结果。

小结

happens-before 是 JMM 给出的"可见性合同":有 hb 关系,就敢读;没有 hb 关系,读到的值就是不可信的。排查诡异的并发 bug,第一步永远是画出两条线程之间的 hb 链,看它是否存在。

笔记加载中…