happens-before 是可见性契约而非执行顺序,它保证A对共享变量的修改对B可见且禁止字长撕裂;常见于程序顺序、volatile读写、锁、start()和join()五种场景,但volatile不保证原子性。

happens-before 不是执行顺序,而是可见性契约
它不保证「A 一定在 B 前面 CPU 执行」,只承诺:如果 A happens-before B,那么 A 对共享变量的修改,对 B 一定是可见的;且 B 不会看到 A 写了一半的 long 或 double(即禁止字长撕裂)。
常见错误现象:volatile 字段没加,线程 B 死循环读 flag == false 永远不退出;或者看到 flag == true 却读到 data 是 0(未初始化值)。
- 根本原因不是“代码写得慢”,而是 JVM 和 CPU 允许重排序 + 缓存不一致
- 只要没建立 happens-before 关系,编译器、JIT、CPU 都可能把
data = 42重排到flag = true后面 - 加
volatile是最轻量的解法——它在写后插入写屏障,在读后插入读屏障,强制刷缓存+禁重排
哪些操作天然构成 happens-before?记住这 5 条硬规则
不用背全 8 条,日常开发真正高频用到的是以下 5 种场景,其他基本靠 synchronized 或 java.util.concurrent 底层封装兜底:
-
程序顺序规则:同一线程内,
x = 1happens-beforey = x + 1(控制流路径中前序操作 → 后续操作) -
volatile 规则:对
volatile变量v的写 happens-before 后续任意线程对该v的读(注意:“后续”指时钟上发生时间晚,不一定是代码写在后面) -
锁规则:
unlock()happens-before 后面对同一把锁的lock()(哪怕发生在不同线程) -
start() 规则:主线程调用
t.start()happens-before 线程t的run()第一行 -
join() 规则:线程
t.join()返回 happens-before 当前线程继续执行下一行(即你能安全读t写过的变量)
容易踩的坑:synchronized 块里改了非 final 字段,但没在 unlock 后立刻被其他线程读——这时仍需靠锁的配对使用来建立关系,单次加锁不能跨方法保可见性。
立即学习“Java免费学习笔记(深入)”;
为什么 volatile 能解决可见性,却不能替代 synchronized?
volatile 保证可见性和禁止重排序,但不保证原子性。这是它和 synchronized 最本质的区别。
- 能用
volatile:状态标志(running = false)、一次性发布(instance != null的 DCL 单例) - 不能用
volatile:counter++(读-改-写三步非原子)、多个字段协同更新(如size和elements[]必须一起变) - 性能影响:X86_64 上
volatile写仅多一条lock xchg指令,开销极小;但频繁写可能触发缓存行失效,间接拖慢邻近变量访问
示例对比:
volatile boolean shutdownRequested = false;
// ✅ 安全:一个线程设为 true,另一个线程一定能及时看到
while (!shutdownRequested) { /* ... */ }
<p>volatile int counter = 0;
// ❌ 危险:两个线程同时执行 counter++,结果可能只 +1
counter++;传递性是隐式桥梁,也是最容易漏掉的逻辑链
happens-before 支持传递:若 A hb B 且 B hb C,则 A hb C。这个特性让看似松散的操作能串成完整可见性链。
- 典型场景:DCL 单例中,构造函数末尾对
instance的 volatile 写 → 后续线程读instance→ 该读又 happens-before 后续对instance字段的访问 - 陷阱在于“中间环节断链”:比如你忘了把
instance声明为volatile,那即使构造完成,其他线程也可能看到部分初始化的对象 - 调试时看不到问题?因为 JIT 优化和 CPU 缓存行为有随机性,问题往往只在高并发、多核、特定负载下暴露
真正难的从来不是记住规则,而是写完代码后,能快速画出线程间所有可能的 happens-before 路径——尤其当涉及多个 volatile 变量、锁嵌套、回调或 Future 时,少画一步就可能埋下偶发 bug。










