happens-before 是 JMM 中一个非常核心且抽象的概念,它是理解多线程内存可见性和有序性的关键。
1. 什么是 happens-before?
happens-before 是一个 偏序关系,用于描述多线程环境下两个操作之间的 内存可见性 和 执行顺序。
如果操作 A happens-before 操作 B,那么:
- 内存可见性:操作 A 执行的结果对操作 B 是 可见的。也就是说,B 操作在执行时,能够看到 A 操作对共享变量所做的所有修改。
- 执行顺序:从 JMM 的角度来看,操作 A 的执行 先于 操作 B。
注意: happens-before 并不意味着操作 A 在物理时间上一定比操作 B 先执行。它是一个 JMM 层面的保证,确保了在符合规则的前提下,多线程程序的行为是可预测的。
2. 为什么需要 happens-before 规则?
在多线程环境中,由于编译器优化、CPU 指令重排序和 CPU 缓存等原因,代码的实际执行顺序和我们编写的顺序可能不同,这会导致线程间的数据竞争和可见性问题。
happens-before 规则为我们提供了一套 “安全网” 。只要我们的代码满足这些规则,JMM 就会保证程序的并发安全性,我们无需关心底层的重排序和缓存细节。
3. JMM 中的 happens-before 规则
JMM 定义了以下几条基本的 happens-before 规则,这些规则是所有并发编程的基础:
1. 程序顺序规则 (Program Order Rule)
在一个 单线程 中,按照代码的书写顺序,前面的操作 happens-before 后面的操作。
这保证了在单线程内,指令的执行看起来是有序的,符合我们的直觉。
示例:
// 同一个线程内
int a = 1; // 操作 A
int b = a + 2; // 操作 B
// 根据程序顺序规则,A happens-before B。
// 所以,B 操作可以看到 A 操作对 a 的赋值(a = 1),因此 b 的结果一定是 3。
2. 监视器锁规则 (Monitor Lock Rule)
一个线程 释放锁 的操作 happens-before 另一个线程 获取同一个锁 的操作。
这是 synchronized 关键字实现线程安全的核心保证。
示例:
Object lock = new Object();
int sharedVar = 0;
// 线程 A
synchronized (lock) { // 获取锁
sharedVar = 1; // 操作 A (释放锁前的最后一个操作)
} // 释放锁 (操作 A')
// 线程 B
synchronized (lock) { // 获取锁 (操作 B')
System.out.println(sharedVar); // 操作 B
} // 释放锁
// 根据监视器锁规则,A' (释放锁) happens-before B' (获取锁)。
// 又因为 A happens-before A' (程序顺序规则),且 B' happens-before B (程序顺序规则),
// 所以 A happens-before B。
// 因此,线程 B 执行操作 B 时,一定能看到线程 A 执行操作 A 对 sharedVar 的修改(sharedVar = 1)。
3. volatile 变量规则 (Volatile Variable Rule)
对一个 volatile 变量的 写操作 happens-before 后续对同一个 volatile 变量的 读操作。
这保证了 volatile 变量的修改对其他线程是立即可见的。
示例:
volatile boolean flag = false;
// 线程 A
flag = true; // 操作 A (写 volatile 变量)
// 线程 B
if (flag) { // 操作 B (读 volatile 变量)
// ...
}
// 根据 volatile 变量规则,A happens-before B。
// 所以,线程 B 执行操作 B 时,能够看到线程 A 对 flag 的修改(flag = true)。
4. 线程启动规则 (Thread Start Rule)
调用 Thread.start() 方法的操作 happens-before 该启动线程内的任意操作。
示例:
Thread threadB = new Thread(() -> {
// 线程 B 内的操作 B
System.out.println("Thread B is running");
});
// 主线程
threadB.start(); // 操作 A
// 根据线程启动规则,A happens-before B。
// 这意味着线程 B 开始执行时,主线程在 start() 之前的所有操作对 B 都是可见的。
5. 线程终止规则 (Thread Termination Rule)
线程内的所有操作都 happens-before 其他线程检测到该线程已经终止的操作。
检测线程终止的方式包括:调用 Thread.join() 并返回、调用 Thread.isAlive() 返回 false 等。
示例:
Thread threadA = new Thread(() -> {
// 线程 A 内的操作 A
sharedVar = 100;
});
threadA.start();
// 主线程
threadA.join(); // 操作 B (检测线程 A 终止)
System.out.println(sharedVar); // 操作 C
// 根据线程终止规则,A happens-before B。
// 又因为 B happens-before C (程序顺序规则),
// 所以 A happens-before C。
// 因此,主线程在操作 C 时,一定能看到线程 A 在操作 A 中对 sharedVar 的修改。
6. 传递性规则 (Transitivity)
如果操作 A happens-before 操作 B,并且操作 B happens-before 操作 C,那么操作 A happens-before 操作 C。
这是一个非常重要的推导规则,它允许我们将多个基本规则组合起来,推断出复杂操作之间的 happens-before 关系。
我们在前面的 synchronized 示例中已经用到了传递性。
总结
happens-before 规则是 JMM 为程序员提供的一套 并发编程的“契约” 。它屏蔽了底层硬件和编译器的复杂细节,让我们能够通过遵守这些规则来编写出正确的多线程程序。
简单来说,只要我们的代码满足 happens-before 规则,我们就可以确保一个线程的修改能被另一个线程正确地看到,从而避免数据竞争和内存可见性问题。