Java Happens-Before 关系详解及案例
Happens-Before 是 Java 内存模型(JMM)中最重要的概念之一,它定义了操作之间的内存可见性保证,我将通过多个案例来详细说明。

什么是 Happens-Before?
Happens-Before 关系保证:如果操作 A Happens-Before 操作 B,A 所做的修改对 B 可见,且 A 的执行顺序在 B 之前。
Java 中的 Happens-Before 规则
/**
* Happens-Before 的 8 种基本规则
*/
public class HappensBeforeRules {
// 规则1:程序顺序规则
// 同一个线程中的每个操作都 happens-before 于该线程中后续的任意操作
// 规则2:监视器锁规则
// 对一个锁的解锁 happens-before 于随后对这个锁的加锁
// 规则3:volatile变量规则
// 对一个 volatile 域的写 happens-before 于任意后续对这个 volatile 域的读
// 规则4:传递性
// A happens-before B,且 B happens-before C,A happens-before C
// 规则5:线程启动规则
// Thread.start() happens-before 于该线程中的任意操作
// 规则6:线程终止规则
// 线程中的所有操作 happens-before 于其他线程检测到该线程终止
// 规则7:中断规则
// 线程的 interrupt() 调用 happens-before 于被中断线程检测到中断
// 规则8:对象终结规则
// 对象的构造函数结束 happens-before 于 finalize() 方法开始
}
具体案例演示
案例1:volatile 变量的 Happens-Before
public class VolatileExample {
private volatile boolean flag = false;
private int value = 0;
/**
* 问题:这个示例中,主线程写入 value 和 flag,
* 读线程能否看到主线程对 value 的修改?
*/
public void volatileDemo() {
// 线程A:写入数据
Thread writer = new Thread(() -> {
value = 42; // 1. 普通写
flag = true; // 2. volatile写
});
// 线程B:读取数据
Thread reader = new Thread(() -> {
while (!flag) { // 3. volatile读
// 等待
}
System.out.println("value = " + value); // 4. 普通读
});
writer.start();
reader.start();
/**
* Happens-Before 分析:
* - 操作1 happens-before 操作2(程序顺序规则)
* - 操作2 happens-before 操作3(volatile规则)
* - 操作3 happens-before 操作4(程序顺序规则)
* - 通过传递性,操作1 happens-before 操作4
* - 所以线程B一定能看到 value = 42
*/
}
}
案例2:synchronized 的 Happens-Before
public class SynchronizedExample {
private int count = 0;
private final Object lock = new Object();
/**
* synchronized 通过锁的获取和释放建立 Happens-Before 关系
*/
public void synchronizedDemo() {
// 线程A:写入数据
new Thread(() -> {
synchronized (lock) {
count = 100; // 1. 在锁内写入
} // 2. 释放锁
}).start();
// 线程B:读取数据
new Thread(() -> {
synchronized (lock) { // 3. 获取锁
// 4. 读取数据,能够看到线程A的修改
System.out.println("count = " + count);
}
}).start();
/**
* Happens-Before 分析:
* - 操作1 happens-before 操作2(程序顺序规则)
* - 操作2 happens-before 操作3(监视器锁规则:解锁happens-before加锁)
* - 操作3 happens-before 操作4(程序顺序规则)
* - 传递性:操作1 happens-before 操作4
*/
}
}
案例3:线程启动的 Happens-Before
public class ThreadStartExample {
private int data = 0;
private boolean initialized = false;
public void threadStartDemo() {
data = 88; // 1. 主线程写入数据
initialized = true; // 2. 设置标志
// 3. 启动新线程
Thread worker = new Thread(() -> {
// 4. 新线程中读取数据
// 通过线程启动规则,一定能看到主线程对 data 和 initialized 的修改
System.out.println("data = " + data);
System.out.println("initialized = " + initialized);
});
worker.start(); // 触发 Happens-Before
/**
* 分析:
* - 操作1、2 在 start() 之前
* - start() happens-before 新线程中的任何操作
* - 所以操作4一定能看到操作1、2的结果
*/
}
}
案例4:正确使用 Happens-Before 的示例
public class CorrectUsageExample {
private volatile boolean running = true;
private int taskCount = 0;
/**
* 实际应用:优雅停机
*/
public void gracefulShutdownDemo() {
Thread worker = new Thread(() -> {
while (running) { // volatile 读
// 执行任务
System.out.println("执行任务 " + (++taskCount));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("线程停止");
});
worker.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 主线程设置运行标志
running = false; // volatile 写
/**
* 通过 volatile 的 Happens-Before 关系,
* worker 线程能够看到 running = false 的修改
*/
}
}
案例5:利用传递性构建 Happens-Before
public class TransitivityExample {
private int a = 0;
private boolean flag = false;
/**
* 使用多个同步机制构建 Happens-Before 链
*/
public void transitivityDemo() {
Thread producer = new Thread(() -> {
synchronized (this) {
a = 10; // 1. 写入普通变量
flag = true; // 2. 写入标志
} // 3. 释放锁
});
Thread consumer = new Thread(() -> {
synchronized (this) { // 4. 获取锁
if (flag) { // 5. 读取标志
int value = a; // 6. 读取普通变量
System.out.println("通过传递性看到 a = " + value);
}
}
});
producer.start();
consumer.start();
/**
* Happens-Before 链:
* 1 -> 2 -> 3(程序顺序)
* 3 -> 4(监视器锁规则)
* 4 -> 5 -> 6(程序顺序)
* 传递性:1 happens-before 6
*/
}
}
破坏 Happens-Before 的错误示例
public class WrongExample {
private int value = 0;
private boolean ready = false;
/**
* 错误示例:没有正确建立 Happens-Before 关系
*/
public void wrongDemo() {
// 线程A:写入数据
new Thread(() -> {
value = 42; // 1. 普通写
ready = true; // 2. 普通写(不是volatile)
}).start();
// 线程B:读取数据
new Thread(() -> {
while (!ready) { // 3. 可能永远看不到 ready 的变化
// busy wait
}
// 4. 即使看到 ready,也可能看不到 value 的最新值
System.out.println("value = " + value);
}).start();
/**
* 问题:
* - 没有 volatile,没有 synchronized
* - 线程B可能永远看不到 ready = true
* - 即使看到 ready,也可能看到 value = 0
* - 这里没有建立任何 Happens-Before 关系
*
* 修正方法:
* - 将 ready 声明为 volatile
* - 或使用 synchronized 块
* - 或使用 AtomicBoolean
*/
}
// 正确的修正版本
public void correctDemo() {
Thread writer = new Thread(() -> {
value = 42;
// 使用 synchronized 确保可见性
synchronized (this) {
ready = true;
}
});
Thread reader = new Thread(() -> {
synchronized (this) {
while (!ready) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("value = " + value);
});
writer.start();
reader.start();
}
}
常见应用场景总结
public class HappensBeforeApplications {
/**
* 1. 双重检查锁定(DCL)- 需要 volatile
*/
private volatile static HappensBeforeApplications instance;
public static HappensBeforeApplications getInstance() {
if (instance == null) { // 第一次检查
synchronized (HappensBeforeApplications.class) {
if (instance == null) { // 第二次检查
instance = new HappensBeforeApplications();
}
}
}
return instance;
}
/**
* 2. 发布对象
*/
private final Map<String, String> config = new HashMap<>();
public void publishConfig() {
config.put("key", "value"); // 在发布前写入
// 确保正确发布,比如通过 volatile 引用或 synchronized
}
/**
* 3. 并发集合中的 Happens-Before
*/
public void concurrentCollections() {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// put 操作 happens-before 后续的 get 操作
map.put("count", 100);
// get 操作能看到 put 的结果
Integer count = map.get("count");
}
}
关键要点
-
Happens-Before 不是时间顺序:它强调的是内存可见性保证,不是物理时间上的先后。
-
传递性很重要:可以通过多个 Happens-Before 关系串联起来。
-
常见误区:
- 认为所有线程都能看到共享变量的修改
- 认为只需要加锁就能保证可见性
- 认为 volatile 能保证原子性
-
最佳实践:
- 优先使用 java.util.concurrent 包中的工具类
- 确保正确使用 volatile、synchronized
- 避免过度优化导致的内存可见性问题
通过理解和使用 Happens-Before 关系,我们可以更好地编写并发程序,避免由内存可见性导致的难以调试的问题。