什么是 happens-before 原则?
Happens-Before 原则是 Java 内存模型(JMM)中定义的一个核心规则集合。它用于描述两个操作之间的内存可见性关系。
如果操作 A “Happens-Before” 操作 B(记为 hb(A, B)),那么:
- A 操作所做的所有内存更改(写),在 B 操作执行时,必定对 B 可见。
- JMM 会禁止编译器与处理器对这两个操作进行重排序,如果这种重排序会违反
hb(A, B)关系。
其根本目的是:在尽可能允许编译器/处理器进行优化的前提下,为程序员提供一个清晰、强内存可见性的承诺,从而简化并发编程。程序员只需遵循这些预定义的规则,就能写出正确同步的代码,而无需关心底层复杂的指令重排序和内存屏障。
深度解析
原理/机制:为什么需要 Happens-Before?
现代计算机和编译器为了极致性能,会进行大量优化:
- 编译器优化:可能调整代码执行顺序。
- 处理器指令级并行:可能乱序执行指令。
- 内存系统缓存:变量可能被缓存在 CPU 缓存中,导致一个线程的修改无法立即被其他线程看到。
这些优化在单线程下完全正确(遵守 as-if-serial 语义),但在多线程下会导致严重的“可见性”和“有序性”问题。JMM 没有选择完全禁止这些优化(那会导致性能灾难),而是抽象出了 Happens-Before 规则。它告诉编译器和处理器:“你们可以任意优化,但必须遵守我定义的这几条 Happens-Before 规则。” 这样,就在性能与编程易用性之间取得了平衡。
八大 Happens-Before 规则
这是理解和回答此问题的关键,必须掌握。
- 程序顺序规则:在同一个线程中,按照控制流顺序,前面的操作 Happens-Before 于后面的任何操作。
- 监视器锁规则:对一个监视器锁的
unlock操作 Happens-Before 于后续对同一个锁的lock操作。 volatile变量规则:对一个volatile变量的写操作 Happens-Before 于后续对这个变量的读操作。- 线程启动规则:
Thread对象的start()方法调用 Happens-Before 于该线程启动后的任何操作。 - 线程终止规则:线程中的所有操作 Happens-Before 于其他线程检测到该线程已经终止(如通过
Thread.join()成功返回或Thread.isAlive()返回false)。 - 线程中断规则:一个线程调用另一个线程的
interrupt()Happens-Before 于被中断线程检测到中断(抛出InterruptedException或调用isInterrupted())。 - 对象终结规则:一个对象的构造函数的结束(
()) Happens-Before 于该对象的finalize()方法的开始。 - 传递性规则:如果
A hb B,且B hb C,那么可以推导出A hb C。
传递性规则是理解复杂同步情况的钥匙。例如,通过 volatile 变量和程序顺序规则的组合,可以实现两个普通变量在不同线程间的可见性传递。
代码示例
以下代码演示了如何利用 volatile 规则和传递性规则来保证两个普通变量的可见性。
public class HappensBeforeExample {
// 使用 volatile 变量作为“触发器”
private volatile boolean flag = false;
private int value = 0; // 这是一个普通变量
public void writer() {
value = 42; // 普通写操作
flag = true; // volatile 写操作
// 根据【程序顺序规则】:(1) hb (2)
// 根据【volatile变量规则】:(2) hb (4)(见reader方法)
}
public void reader() {
if (flag) { // volatile 读操作 (3)
// 根据【volatile变量规则】:(2) hb (3)
// 根据【程序顺序规则】:(3) hb (4)
// 根据【传递性规则】:(1) hb (2) hb (3) hb (4) => (1) hb (4)
// 因此,这里一定能看到 value == 42
System.out.println("value is: " + value); // (4) 普通读操作
}
}
}
最佳实践与常见误区
- 最佳实践:
- 在编写并发代码时,主动依赖这些规则来保证正确性。例如,使用
synchronized(锁规则)、volatile(volatile 变量规则)或并发容器(其内部已实现这些规则)来同步共享数据。 - 理解
final域的特殊语义:在构造过程中正确初始化的final域,其值在不使用同步的情况下也能安全地被其他线程看到,这背后也有特定的 Happens-Before 保证。
- 在编写并发代码时,主动依赖这些规则来保证正确性。例如,使用
- 常见误区:
- Happens-Before 不等于时间上的先后。它定义的是可见性的保证顺序,而不是绝对的时间顺序。即使 A hb B,在时间上 B 也可能先于 A 的某些部分执行完成(只要不影响可见性)。
- 两个操作没有 Happens-Before 关系,并不意味着它们一定会被重排序。JMM 允许重排序,但不强制。实际的执行顺序是不确定的。
- 错误地认为 “线程内顺序就是执行顺序”。单一线程内,虽然没有 “可见性” 问题,但为了优化,只要不影响该线程的最终结果,指令仍可能被重排序。Happens-Before 的程序顺序规则保证了这种重排序对该线程自身观察到的结果是透明的。
浙公网安备 33010602011771号