happens-before 是 JVM 内存模型的顺序契约,定义写操作对读操作的可见性而非执行顺序;满足该关系才保证读线程看到最新值,常见规则包括 volatile 写→读、锁释放→获取、start→run、join 返回→后续操作、final 字段安全发布等。

happens-before 是 JVM 内存模型的“顺序契约”,不是执行顺序保证
它不决定代码实际怎么跑,而是定义哪些写操作对哪些读操作“可见”。只要满足 happens-before 关系,JVM 就必须确保读线程能看到之前写线程的最新值;不满足时,即使代码看起来是先后写的,读线程也可能看到过期值、0、null 或任意中间态。
常见误判是把 synchronized 块的执行先后等同于 happens-before——其实只有加锁/解锁配对才构成关系,比如线程 A 释放锁后,线程 B 获取同一把锁,才建立 A 对 B 的 happens-before。
- volatile 写 happens-before 后续对该变量的 volatile 读
- start() 调用 happens-before 线程 run() 中的任意操作
- 线程所有操作 happens-before 其 join() 返回
- 同一个 monitor 的 unlock happens-before 后续对同一 monitor 的 lock
volatile 为什么能禁止重排序但不能替代锁
volatile 的语义包含两层:对变量的读写直接走主内存(不缓存到寄存器或 CPU cache),以及插入内存屏障(memory barrier)禁止编译器和处理器对它前后指令的重排序。但它不提供原子性——count++ 这种读-改-写操作在 volatile 变量上仍是竞态的。
典型陷阱是以为给 boolean 标志位加了 volatile,就能安全控制整个初始化流程。如果初始化涉及多个字段,仅靠一个 volatile flag 无法保证其他字段的写入对读线程可见(缺少 happens-before 链)。
立即学习“Java免费学习笔记(深入)”;
- 正确做法:把所有需同步的字段一起纳入同步块,或用
final字段 + 构造器安全发布 - 错误写法:
flag = true前没同步写其他字段,读线程看到flag == true却读到未初始化的字段值 - volatile 适合状态标志、一次性事件通知(如 shutdown)、轻量级读多写少场景
Thread.join() 的 happens-before 效果常被低估
调用 thread.join() 并返回,意味着该线程已终止,且它执行期间的所有操作(包括对共享变量的写)都对当前线程可见。这个关系比很多人想的更强:它不只是“线程结束”,而是“它做过的所有事我都看得到”。
注意它只对调用方生效,不影响其他线程。如果两个线程都 join 同一个线程,它们彼此之间没有 happens-before 关系。
- 适用场景:主线程等待 worker 线程完成计算并消费结果,无需额外同步
- 等价于在 worker 线程末尾隐式执行了 flush,主线程 join 返回后可直接读 sharedResult
- 不要用
thread.isAlive()替代join()—— 它不建立 happens-before,无法保证可见性
final 字段的 happens-before 是安全发布的底层保障
对象构造器中对 final 字段的赋值,会在构造完成时对其他线程“立即可见”——前提是该对象本身是安全发布的(如通过 volatile 引用、static final、锁内发布等)。JVM 为此在构造器结尾插入 StoreStore 屏障,防止 final 字段写被重排序到构造器外。
一旦绕过安全发布(比如把 this 引用逸出到构造器中),final 的保障就失效,其他线程可能看到 partially initialized 对象。
- 禁止在构造器里注册监听器、启动线程、放入全局 map——这些都会导致 this 逸出
- 静态工厂方法 + private 构造器 + final 字段是推荐的安全发布模式
- final 只保护字段引用本身不可变,不保护其指向对象的内部状态(如
final List的元素仍可被修改)
真正难的是把 happens-before 链串起来:一个 volatile 写、一次锁释放、一个 join 返回……这些点要连成完整路径,才能让远端读取看到预期结果。漏掉任意一环,看似合理的并发逻辑就会在高并发或特定 CPU 架构下崩塌。









