happens-before 是 Java 内存模型定义的语义规则,用于保证多线程操作的可见性与有序性;它不依赖执行时间或代码顺序,而是通过程序顺序、锁、volatile、线程启动/终止等规则及传递性建立约束。

Java 中多线程动作的先后顺序不能只看代码书写顺序或执行时间,而是靠 happens-before 关系来定义“可见性”和“有序性”。它不是运行时检测的机制,而是一组编译器、JVM 和处理器必须共同遵守的**语义规则**——只要两个操作之间存在 happens-before 关系,前一个操作的结果(如变量写入)对后一个操作(如变量读取)就是可见的,且不会被重排序破坏。
哪些场景天然建立 happens-before 关系
Java 内存模型(JMM)明确定义了以下几类操作自动构成 happens-before:
- 程序顺序规则:同一个线程中,按源码顺序排列的两个操作,前面的操作 happens-before 后面的操作(即使因指令重排序改变了实际执行顺序,JVM 仍保证该语义)。
- 监视器锁规则:对一个锁的解锁操作 happens-before 后续对该锁的加锁操作(即 synchronized 块的退出一定 happens-before 下一个线程进入同一把锁的同步块)。
- volatile 变量规则:对一个 volatile 变量的写操作 happens-before 后续对该变量的读操作(无论是否同一线程)。
- 线程启动规则:Thread.start() 的调用 happens-before 新线程中任意动作。
- 线程终止规则:线程中所有操作都 happens-before 其他线程检测到该线程已结束(如 join() 返回、isAlive()==false)。
- 中断规则:线程 A 调用线程 B 的 interrupt() happens-before 线程 B 检测到中断(如 isInterrupted() 或抛出 InterruptedException)。
- 终结器规则:对象的构造函数执行完成 happens-before 它的 finalize() 方法开始执行。
- 传递性:如果 A happens-before B,且 B happens-before C,则 A happens-before C(这是推导跨操作关系的关键)。
怎么用 happens-before 判断两个动作是否有序
判断步骤很直接:
- 找出两个目标动作(例如:线程1写变量 x=1;线程2读变量 x)。
- 检查是否存在一条由 JMM 规则组成的 happens-before 链,从写操作连到读操作(可跨线程、跨锁、跨 volatile)。
- 若链存在,则写对读可见、不重排序;否则,JMM 不保证,可能看到旧值、甚至出现指令重排导致逻辑错乱。
例子:线程1执行 x = 1; synchronized(lock) { y = 1; },线程2执行 synchronized(lock) { r1 = x; }。 分析:线程1中 x=1 → y=1(程序顺序);y=1 的写 → 线程2中 lock 的 acquire(监视器锁规则);线程2中 acquire → r1=x(程序顺序)。由传递性得:x=1 happens-before r1=x。所以线程2一定能读到 x=1。
常见误区:happens-before ≠ 执行先后
它不描述真实时间上的“谁先运行”,而是一种**语义约束**:
- 即使线程2比线程1早启动,只要满足 happens-before 链,它的读仍能看到线程1的写。
- 没有 happens-before 关系的动作,即便实际执行有先后,JVM 也可能让后启动的线程看不到前者的写(比如普通变量无同步)。
- happens-before 不是性能指标,也不触发强制刷新缓存——它是对重排序和可见性的底线承诺,底层实现(如内存屏障)由 JVM 自行决定。
如何确保你写的代码有 happens-before
别靠猜,主动建立:
- 共享变量用 volatile(适合简单状态标志,如 running = false)。
- 临界区用 synchronized 或 ReentrantLock(配合 unlock/lock,利用监视器锁规则)。
- 线程协作用 join()、CountDownLatch、CyclicBarrier 等——它们内部基于上述规则封装,自带 happens-before 保证。
- 避免依赖“看起来会按顺序执行”的普通变量读写,尤其在不同线程间传递状态时。
不复杂但容易忽略。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











