happens-before 是定义操作间可见性与顺序约束的偏序关系规则,而非强制执行排序;JVM/CPU 可在不破坏其规则前提下自由重排。

happens-before 不能直接“保证有序性”,它定义的是可见性与动作顺序的约束关系
很多人误以为 happens-before 是 JVM 对代码执行顺序的强制排序指令,其实它是一组**偏序关系规则**,用于判断一个操作的结果对另一个操作是否可见、是否可被重排。JVM 和 CPU 可以在不破坏任何 happens-before 关系的前提下自由重排序——这才是它真正的作用边界。
常见误解场景:
- 写完
volatile变量后,后续普通变量赋值仍可能被重排到其前面(只要不违反该 volatile 写与读之间的 happens-before) -
synchronized块内语句之间没有额外的 happens-before 保证,仅靠锁的获取/释放建立跨线程约束 - 两个无共享变量、无同步操作的线程,即使逻辑上“先 A 后 B”,也不存在 happens-before,结果不可预测
6 条核心 happens-before 规则及其典型误用点
JSR-133 明确列出的规则中,最容易被忽略或错用的是这几条:
-
程序顺序规则:同一个线程内,按代码顺序,每个操作 happens-before 之后的操作 —— 但仅限于该线程视角,不构成跨线程保证 -
监视器锁规则:解锁unlock()happens-before 后续对同一锁的加锁lock()—— 注意是“后续”加锁,不是任意加锁;如果线程 B 没有真正 acquire 到锁,就不触发该规则 -
volatile 变量规则:对 volatile 变量的写 happens-before 后续对它的读 —— “后续”指在执行时序上发生得更晚,且必须是**同一个变量**;volatile int a的写不保护int b的读写 -
线程启动规则:Thread.start()happens-before 该线程第一个动作 —— 但不保证主线程里 start() 之前的其他写操作对子线程可见,除非通过 final 字段、volatile 或锁传递 -
线程终止规则:线程中所有操作 happens-before 其他线程检测到该线程已结束(如t.join()返回)—— 这是唯一能安全获取子线程最终状态的机制之一 -
中断规则:对线程interrupt()happens-before 被中断线程检测到中断(如isInterrupted()返回 true)—— 但不保证中断前的内存写一定可见,需配合同步手段
final 字段的 happens-before 特殊保障:构造安全的关键
对象构造完成(构造函数返回)时,对 final 字段的写,happens-before 于任何其他线程看到该对象引用后的读取 —— 这是 JVM 对 final 的硬性保证,不依赖同步,也不受重排序影响。
立即学习“Java免费学习笔记(深入)”;
但前提是:
- 构造过程中没有泄露
this引用(如在构造函数中启动线程、注册监听器、赋值给静态变量等) - 字段声明为
final,且只在构造函数内赋值一次 - 读取方拿到的是通过合法途径发布的引用(比如经由 volatile、锁、安全发布容器如
ConcurrentHashMap)
否则,即使字段是 final,也可能看到默认值或部分初始化状态。
为什么 synchronized 和 volatile 不等价?看 happens-before 链如何断裂
两者都建立 happens-before,但粒度和传播能力不同:
-
synchronized建立的是“锁释放 → 锁获取”的跨线程链,可串联多个线程(A 释放 → B 获取 → B 释放 → C 获取),天然支持多跳传递 -
volatile是“写 → 读”一对一绑定,无法中继:A 写 x,B 读 x 并写 y,C 读 y —— C 看不到 A 的写,除非 y 也是 volatile - 性能差异背后是内存屏障类型不同:
volatile写插入StoreStore + StoreLoad,而synchronizedunlock 插入StoreStore + StoreLoad + LoadLoad + LoadStore
一个典型断裂场景:用 volatile 标志位通知“数据就绪”,但未用 volatile 或锁保护实际数据本身,读线程可能看到标志为 true 却读到未更新的数据字段。
真正难的不是记住规则,而是判断哪条规则在你的具体调用路径中生效、有没有断链、有没有隐式依赖未被覆盖。多数并发 bug 都出在这里,而不是语法写错。









