happens-before 是 java 内存模型中保障多线程下变量可见性与有序性的唯一硬性依据,它定义操作 a 对 b 的结果可见且不重排序,但不保证原子性、不等价于时间先后、不跨线程自动传递。

Happens-Before 原则不是语法糖,也不是 JVM 的优化开关——它是 Java 内存模型(JMM)里唯一能让你在多线程下「放心读到最新值」的硬性依据。 没有它,就算你用了 synchronized、volatile 或 Lock,编译器和 CPU 仍可能重排序、缓存不一致、读到过期数据。
什么是 happens-before?别被术语吓住,它就两条铁律
它定义的是「操作 A 是否对操作 B 可见」:如果 A happens-before B,那么 A 的结果(比如变量写入)一定对 B 可见,且 A 不会重排序到 B 之后。
- 它不等于「时间上先发生」——
System.currentTimeMillis()返回的时间戳不能用来判断 happens-before - 它不传递给无关线程——线程 T1 中 A → B,T2 中 C → D,但 T1 和 T2 无同步关系,则 A 对 D 不保证可见
- 它只在 JMM 层面起作用,和操作系统调度、CPU 核心缓存刷新时机无关,但依赖 JVM 对这些底层行为的建模
八条规则里真正常踩坑的只有这三条
官方说八条,但日常开发中,90% 的问题出在以下三个场景,其余多是组合推导或边界情况。
-
synchronized锁释放 happens-before 同一锁的获取:注意「同一把锁」——obj1上的synchronized释放,对obj2的获取无效 -
volatile写 happens-before 后续任意线程对该volatile变量的读:但对非volatile字段无扩散效应(常见误以为「写了 volatile 就等于 flush 了整个缓存行」) - 线程
start()happens-before 该线程的任意动作;线程所有动作 happens-before 其join()返回:注意join()是阻塞调用,不是「线程结束瞬间」生效——没等join()就去读共享变量,照样可能读到旧值
为什么加了 volatile 还读不到新值?检查这三件事
这是最典型的「以为满足 happens-before,其实链断了」。
- 确认读写的是同一个
volatile变量名——flag是 volatile,但代码里读的是flagCopy = flag,再读flagCopy,那就完全不参与 happens-before 链 - 确认没有绕过 volatile 的间接引用——比如
volatile List<integer> list</integer>,然后执行list.add(1),add 本身不具 volatile 语义,list 引用虽可见,但内部状态变更不可见 - 确认没有 JIT 优化干扰——极少数情况下(如空循环 + volatile 读),JIT 可能将读提升到循环外(可通过
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly观察,但生产环境几乎不用深究)
happens-before 不是万能锁,别指望它替代同步逻辑
它只管「可见性」和「有序性」,不管「原子性」。比如 i++(读-改-写)即使 i 是 volatile,依然会丢失更新。
-
volatile int counter+ 多线程counter++→ 结果必然小于预期,因为counter++不是原子操作,happens-before 只保证每次读到最新值,不保证两次读之间没被别人改过 - 想靠 volatile 实现状态机流转(如
state = INIT → RUNNING → DONE)可以,但一旦涉及「检查后执行」(if state == RUNNING then doWork()),就必须配合synchronized或AtomicInteger.compareAndSet - JMM 不承诺「全局单调时钟」——两个线程各自观察到的 happens-before 关系可能不一致(虽然实际极少影响业务),所以不要用它做分布式序号生成
真正难的从来不是背八条规则,而是每次写共享变量前,心里得清楚:这条读/写操作,落在哪条 happens-before 链上?链的起点和终点是否真被同一线程或同步机制覆盖?漏掉一个环节,就回到「看似正确、实则竞态」的状态。










