
happens-before 是什么:不是执行顺序,而是可见性契约
它不是说「A 一定在 B 前面 CPU 执行」,而是向程序员承诺:如果 A happens-before B,那么 A 对共享变量的修改,对 B 一定是可见的;且 B 不能看到 A 之前某个中间态(比如只写了一半的 long 值)。这个保证由 JVM 在编译期插内存屏障、运行时配合 CPU 指令(如 lock xchg 或 mfence)共同实现。
- 常见误解:把 happens-before 当作「时间先后」——错。两个操作即使 A 在时钟上晚于 B,只要满足规则(比如 volatile 写后读),仍可构成 happens-before
- 真正起作用的是规则链:单靠一个 volatile 读写不够,得靠传递性串起整个逻辑路径
- as-if-serial 只管单线程;happens-before 是唯一跨线程提供语义保障的机制
哪些场景天然成立 happens-before 关系
不用加锁、不用 synchronized,JMM 就已为你埋好安全边界。但必须严格匹配条件,漏掉任一细节就失效。
-
程序顺序规则:同一线程内,
x = 1happens-beforey = x + 1—— 但若y = x + 1被重排序到x = 1前(无数据依赖时),JVM 仍可优化,只要最终结果等价;而 happens-before 保证的是「B 能看到 A 的结果」,不是禁止重排本身 -
volatile 变量规则:对
flag(声明为volatile boolean flag)的写,happens-before 后续任意线程对该flag的读。注意「后续」指时钟时间上之后,且该读必须发生在写之后(否则可能读到旧值) -
监视器锁规则:线程 A 执行
unlock(),happens-before 线程 B 后续对同一把锁的lock()—— 这就是为什么 synchronized 块退出后,临界区修改对下一个获取锁的线程可见 -
线程启动/终止规则:
t.start()happens-beforet中第一个动作;t最后一个动作 happens-before 其他线程中t.join()返回 —— 所以你在join()后读共享变量是安全的
为什么 volatile 不能替代 synchronized
因为 happens-before 链不完整。volatile 只能保单个变量的写→读可见性,无法保证复合操作的原子性或多个变量间的约束关系。
- 典型翻车现场:
volatile int counter;+counter++—— 读、改、写三步不构成原子操作,其他线程可能在「读完 counter=5」和「写回 counter=6」之间插入自己的写,导致丢失更新 - 再比如双检锁单例:
if (instance == null) { synchronized(...) { if (instance == null) instance = new Singleton(); } },若instance不是 volatile,new 操作可能被重排序为「分配内存→写引用→初始化对象」,导致其他线程拿到未初始化完成的对象引用 - volatile 提供的是「写后读」的定向通道,synchronized 提供的是「进入前清空本地内存 + 退出时刷回主存」的全量同步
调试 happens-before 失效的常见线索
现象往往隐蔽:偶尔出错、压测才暴露、换个 CPU 架构就崩。别急着加锁,先看是否破坏了规则前提。
立即学习“Java免费学习笔记(深入)”;
- 看到
Thread#isAlive()返回 false 后仍读到旧值?检查是否依赖了「线程终止规则」——但你没调用join(),只是轮询isAlive(),这不触发 happens-before - 用
AtomicInteger但结果不一致?确认是否混用了非原子操作:比如ai.get() + 1再ai.set(),这不是 CAS,也不受 happens-before 保护 - 日志里发现变量值「跳变」或「回退」(比如从 2 变成 1)?大概率是缺少 volatile / 锁,导致线程看到不同缓存副本,JMM 不保证这种读的顺序一致性
- 在 Lambda 或匿名内部类里访问外部局部变量,又在线程间共享?Java 要求该变量是「effectively final」,否则编译不过——这是编译器帮你守住程序顺序规则的第一道防线
最易被忽略的一点:happens-before 是偏序关系,不是全序。两个操作如果没有规则覆盖,也没有传递链连接,那它们就是并发的,JVM 和 CPU 都可以自由重排。别指望「看起来应该先发生」就能得到保证——代码里没写明规则,就等于没发生。










