🚦 1、简述
在多线程开发中,我们经常会遇到“明明修改了变量,为什么另一个线程却读取不到”的问题。这并不是代码写错,而是你忽略了 Java 内存模型(JMM) 中的核心规则之一 —— happens-before 原则。
📘 2、什么是 happens-before 原则?
happens-before
是 Java 内存模型(JMM)中的一条可见性规则,它规定了一个操作对另一个操作的执行顺序和可见性。
如果一个操作 A happens-before 操作 B,那么 A 的结果对 B 是可见的,并且 A 的执行排在 B 之前。
换句话说,A 先发生,B 后发生,并且 B 能“看到”A 的结果。
JMM 中的几个 happens-before 规则:
🔹 程序顺序规则
同一个线程中的操作,按照代码顺序依次执行。
🔹 锁定规则(synchronized)
对一个锁的解锁(unlock
)操作,happens-before 之后对这个锁的加锁(lock
)操作。
🔹 volatile 变量规则
对 volatile
变量的写,happens-before 对这个变量的读。
🔹 线程启动规则
Thread.start()
之前的操作,对该线程内可见。
🔹 线程终止规则
Thread.join()
之后主线程能看到子线程结果。
🔹 传递性规则
如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
🛠️ 3、常见实践样例
示例 1:程序顺序规则(单线程)
int a = 10;
int b = a + 1;
System.out.println(b);
这里 a = 10
happens-before b = a + 1
,因为它们在同一线程中按顺序执行。
示例 2:synchronized 保证可见性
class Counter {
private int count = 0;
public synchronized void increment() {
count++; // 写操作
}
public synchronized int getCount() {
return count; // 读操作
}
}
synchronized
保证了 increment()
中的写,对 getCount()
是可见的。
示例 3:volatile 变量的可见性
class FlagDemo {
private volatile boolean flag = false;
public void writer() {
flag = true; // 写
}
public void reader() {
while (!flag) {
// 循环等待直到 flag 为 true
}
System.out.println("Flag changed!");
}
}
使用 volatile
保证 writer()
的写操作对 reader()
是可见的。
示例 4:Thread.start() 之前的 happens-before
public class StartExample {
static int value;
public static void main(String[] args) throws Exception {
value = 42; // 写操作
Thread t = new Thread(() -> {
System.out.println(value); // 能看到 value = 42
});
t.start(); // happens-before 线程内读取
}
}
示例 5:Thread.join() 之后的可见性
public class JoinExample {
static int result = 0;
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> {
result = 100;
});
t.start();
t.join(); // 主线程等待 t 执行完成
System.out.println(result); // 一定是 100
}
}
⚠️ 4、如果没有 happens-before,会发生什么?
示例:没有 volatile 的读写
class NoVisibility {
private boolean ready = false;
public void writer() {
ready = true;
}
public void reader() {
while (!ready) {
// 无限等待
}
System.out.println("Ready is true");
}
}
这里 reader()
可能永远看不到 ready = true
,因为没有构建 happens-before 关系。
🧠 5、总结
🔹 使用 volatile
标记简单状态变量,保证线程间通信可见性。
🔹 对于复杂共享变量使用 synchronized
或 Lock
。
🔹 明确场景构建 happens-before 保证,避免“看不到”的问题。
🔹 如果你使用 Java 并发包(如 AtomicInteger
、CountDownLatch
),它们内部都已封装了 happens-before。
规则 | 示例 | 应用场景 |
---|---|---|
程序顺序 | 同线程代码顺序 | 基本逻辑 |
synchronized | 锁写 -> 锁读 | 并发安全类 |
volatile | 写 volatile -> 读 volatile | 状态标识 |
Thread.start | 启动前变量 -> 线程中可见 | 线程初始化 |
Thread.join | 线程结束 -> 主线程可见 | 子线程计算值 |