什么是 happens-before 原则?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 语义理解:面试官不仅仅是想知道 happens-before 的几条规则,更是想知道你是否理解它的本质——不是 "时间上的先后",而是 "可见性的保证"。这个区别搞不清,背再多规则也是白搭。

  2. 规则记忆与应用:考察你是否能说出 8 条核心规则中的至少 5 条,并且能结合代码解释 "为什么 A 的结果对 B 可见"。

  3. 常见误区识别:很多人以为 happens-before 就是 "先发生",以为时间上先执行的代码一定对后执行的代码可见。面试官就是在等你踩这个坑。

核心答案

先甩结论:happens-before 是 JMM 定义的一套规则,它规定了哪些操作的结果对后续操作可见。如果操作 A happens-before 操作 B,那么 A 的结果对 B 保证可见(A 修改的变量,B 一定能看到最新值)。

核心要点:happens-before 是 "可见性保证",不是 "时间顺序"。即使操作 A 在时间上先于操作 B 执行,如果没有 happens-before 关系,A 的结果也不一定对 B 可见。

深度解析

一、为什么需要 happens-before?

先看一个让人头秃的问题:

// 线程 1                    // 线程 2
int a = 1;                   while (flag == 0) { }
flag = 1;                    int b = a;

问:线程 2 中 b 的值一定是 1 吗?

答案是:不一定。即使线程 1 先执行完 a = 1flag = 1,由于编译器重排、CPU 缓存等原因,线程 2 可能看到 flag == 1a 还是 0。

那怎么才能保证 b == 1?必须让 a = 1 happens-before int b = a。只有建立了 happens-before 关系,JMM 才会给你 "可见性保证"。

这就是 happens-before 存在的意义——给程序员一套 可依赖的规则,告诉你 "在什么条件下,一个线程写的结果对另一个线程可见"。

二、8 条核心规则

JMM 定义了 8 条 happens-before 规则。面试中能说出 5 条以上的基本过关:

序号 规则 含义 举例
1 程序顺序规则 同一线程中,前面的操作 hb 后面的操作 线程内 a=1; b=a;a=1 hb b=a
2 volatile 变量规则 volatilehb 后续对该变量的读 线程 A 写 volatile v,线程 B 读 v,写 hb
3 锁规则 unlock 操作 hb 后续对同一把锁的 lock 线程 A 释放锁,线程 B 获取同一把锁
4 传递性 A hb B,B hb C → A hb C 通过中间变量传递可见性
5 线程启动规则 Thread.start() hb 该线程的每一个操作 主线程 start() 之前的修改,子线程一定可见
6 线程终止规则 线程的所有操作 hb 该线程的 Thread.join() 返回 子线程的修改,join() 后主线程一定可见
7 线程中断规则 interrupt() 调用 hb 被中断线程检测到中断 Thread.interrupted() 的检测
8 对象终结规则 对象的构造函数执行结束 hb finalize() 开始 构造函数中的赋值,finalize 中可见

其中 1、2、3、4、5 这五条是面试高频,必须能结合代码解释清楚。

三、逐条拆解 + 代码示例

规则 1:程序顺序规则(单线程内的基本保证)

// 同一线程内,按代码顺序,前面的操作 happens-before 后面的操作
int a = 1;   // ①
int b = a;   // ②
int c = b;   // ③
// ① hb ②,② hb ③,由传递性 → ① hb ③

这个看着像废话,但其实不是。它不是说 CPU 真的按顺序执行(编译器和 CPU 可以重排),而是说 重排后的结果必须和按顺序执行一致as-if-serial 语义)。

规则 2:volatile 变量规则(跨线程可见性的关键)

// 线程 A                  // 线程 B
data = 42;                 while (!ready) { }
volatile boolean ready = true;   // volatile 写
                           int d = data; // ready 为 true 后,data 一定可见

为什么线程 B 读到 ready == true 后,data 一定是 42?靠的是 传递性

  • data = 42 hb ready = true(程序顺序规则,同一线程)
  • ready = true(volatile 写)hb ready 的读(volatile 规则,线程 B 读到 true
  • 由传递性 → data = 42 hb data 的读取

所以 data = 42 对线程 B 可见。这就是 volatile 最核心的用法——不是只保护自己这一个变量,而是通过 happens-before 传递性,保护它 之前 所有非 volatile 变量的写入。Doug Lea 这个设计确实优雅。

规则 3:锁规则(synchronized 的可见性保证)

// 线程 A                          // 线程 B
synchronized (lock) {              synchronized (lock) {
    x = 1;  // 修改共享变量            int y = x; // 一定能看到 x == 1
}  // unlock                        } // lock

线程 A 在 unlock 之前的修改,线程 B 在后续 lock 之后一定能看到。这就是为什么 synchronized 能保证可见性——不是魔法,是 happens-before 锁规则在起作用。

规则 5:线程启动规则

int config = loadConfig();   // 主线程初始化配置

Thread t = new Thread(() -> {
    useConfig(config); // 子线程一定能看到完整的 config
});
t.start(); // start() happens-before 子线程的所有操作

start() 之前主线程做的所有修改,子线程都可见。这也是 InheritableThreadLocal 能工作的基础——父线程在 start() 前设置的数据,子线程一定能看到。

规则 6:线程终止规则

Thread t = new Thread(() -> {
    result = doHeavyWork(); // 子线程修改了 result
});
t.start();
t.join(); // join() 返回后,result 一定对主线程可见

System.out.println(result); // 一定能拿到最新值

join() 返回意味着子线程已经结束,子线程中的所有修改对调用 join() 的线程可见。

四、最大误区:happens-before ≠ 时间先后

这个误区太常见了。面试官最爱用这个来挖坑:

// 在同一个线程中
int x = 1;   // 操作 A
int y = 2;   // 操作 B

问:操作 A happens-before 操作 B 吗?

是的,因为程序顺序规则。

再问:操作 A 在时间上一定先于操作 B 执行吗?

不一定!编译器或 CPU 可能把它们重排,甚至并行执行。但因为 happens-before 保证了操作 A 的结果对操作 B 可见,而操作 B 并不依赖操作 A 的结果,所以重排不影响正确性。

反过来:

// 线程 1        // 线程 2
x = 1;           y = 1;

问:线程 1 的 x = 1 在时间上先于线程 2 的 y = 1,那 x = 1 happens-before y = 1 吗?

不是!两个不同线程之间没有同步关系,没有 happens-before。即使时间上先执行,也不保证可见。

所以:happens-before 是 "可见性保证",不是 "时序描述"。这句话面试时一定要说,说了就是加分项。

五、一张图总结 8 条规则的逻辑关系

上图把 8 条规则分成了两层:

  • 单线程内:程序顺序、中断、终结这三条保证了单个线程内部的可见性
  • 跨线程同步volatile、锁、startjoin 这四条是线程之间建立 happens-before 的桥梁
  • 传递性是 "粘合剂",把单线程和跨线程的规则串起来,形成完整的可见性链路

还有一个容易忽略的点:没有 happens-before 关系不等于一定不可见。JMM 只是 "不保证" 可见,实际执行中可能可见也可能不可见,取决于硬件和编译器的具体行为。所以没有 happens-before 保证的代码就是 "有 bug 的代码",不要依赖运气。

面试高频追问

  1. happens-before 和 "时间上的先后" 有什么区别?

    happens-before 是 JMM 定义的 可见性保证:A hb B 意味着 A 的修改对 B 可见。而 "时间上的先后" 只是说 A 比 B 先执行,但不保证可见。两个不同线程的操作即使有时间上的先后,如果没有 hb 关系,也不保证可见。

  2. volatile 怎么通过 happens-before 保证可见性的?

    volatilehb 后续对同一变量的 volatile 读。再配合程序顺序规则和传递性,volatile 写之前的所有修改(包括非 volatile 变量)都能被读到 volatile 值的线程看到。这就是 volatile 实现轻量级同步的原理。

  3. synchronizedhappens-before 体现在哪?

    体现在锁规则:unlock hb 后续对同一把锁的 lock。线程 A 在 synchronized 块中的所有修改,在 A 释放锁后,线程 B 获取同一把锁后一定可见。这也是 synchronized 保证可见性的底层依据。

常见面试变体

  • "说说 happens-before 有哪些规则?"
  • "volatile 是怎么保证可见性的?从 happens-before 角度解释"
  • "synchronized 的可见性是怎么保证的?"
  • "为什么双重检查锁定单例要加 volatile?"

记忆口诀

本质happens-before 是 "可见性保证",不是 "时间先后"

核心五条:程序顺序、volatile 写→读、unlocklock、传递性、startrun

传递性是关键:通过中间的 volatile/锁,把前面的修改 "传" 到后面的线程

总结

happens-before 是 JMM 的核心规则,它定义了 "哪些操作的结果对后续操作可见"。8 条规则中,程序顺序、volatile、锁、传递性、线程启动这五条是面试重点。回答时一定要强调 "happens-before 是可见性保证而非时间顺序",这一句话就能拉开你和大部分候选人的差距。配合具体代码场景把传递性的推导过程讲出来,这道题满分。