在多线程的世界里,时间不是线性的!happens-before告诉我们:什么会先发生,什么能被看见。
一、开场:诡异的多线程现象👻
问题:这段代码会输出什么?
public class StrangeCode {
private static int a = 0;
private static boolean flag = false;
public static void main(String[] args) {
// 线程1
new Thread(() -> {
a = 1; // ①
flag = true; // ②
}).start();
// 线程2
new Thread(() -> {
if (flag) { // ③
System.out.println(a); // ④ 输出什么?
}
}).start();
}
}
可能的输出:
1(正常)- 什么都不输出(flag还是false)
0(诡异!) 😱
为什么会输出0?
因为:
- 编译器可能重排序:先执行②再执行①
- CPU缓存可能不一致:线程2看不到a=1
- 没有happens-before关系保证!
二、什么是happens-before?🤔
官方定义
如果一个操作happens-before另一个操作,那么第一个操作的结果对第二个操作可见。
通俗理解:
happens-before是Java内存模型(JMM)定义的一套规则,告诉我们:
- 哪些操作是有序的(不会被重排序)
- 哪些写入是可见的(能被其他线程看到)
生活类比:
想象你给朋友发微信消息📱:
- 没有happens-before: 你发"到了"和"在门口"两条消息,朋友可能先看到"在门口"再看到"到了"(乱序)
- 有happens-before: 微信保证消息按发送顺序到达(有序且可见)
三、happens-before的8条黄金法则📜
法则1️⃣:程序顺序规则(Program Order Rule)
规则: 在一个线程内,代码按照书写顺序执行(表面上看)。
int a = 1; // ①
int b = 2; // ②
int c = a + b; // ③
// ① happens-before ②
// ② happens-before ③
注意: 这只是在单线程内看起来有序,实际可能重排序(只要不影响结果)。
例子:可以重排序
int a = 1; // ①
int b = 2; // ② 和①无依赖,可以重排序
int c = a + 1; // ③ 依赖①,不能排到①前面
实际执行可能是:②→①→③
生活类比:
你做早餐🍳:
- 煮咖啡(①)
- 烤面包(②)
- 涂果酱(③,依赖②)
①和②可以同时做(重排序),但③必须在②之后。
法则2️⃣:volatile变量规则(Volatile Variable Rule)
规则: 对volatile变量的写,happens-before后续对该变量的读。
public class VolatileExample {
private int a = 0;
private volatile boolean flag = false;
// 线程1
public void writer() {
a = 1; // ①
flag = true; // ② volatile写
}
// 线程2
public void reader() {
if (flag) { // ③ volatile读
System.out.println(a); // ④ 一定输出1!
}
}
}
保证:
- ② happens-before ③(volatile规则)
- ① happens-before ②(程序顺序规则)
- ③ happens-before ④(程序顺序规则)
- 根据传递性:① happens-before ④
所以a=1对线程2可见!
原理:
volatile写入时,JMM会:
- 禁止之前的写操作重排序到volatile写之后
- 刷新缓存到主内存
volatile读取时,JMM会:
- 禁止之后的读操作重排序到volatile读之前
- 从主内存读取最新值
内存屏障:
写操作:
普通写
普通写
StoreStore屏障 ← 禁止重排序
volatile写
StoreLoad屏障 ← 立即刷新
读操作:
LoadLoad屏障 ← 清空缓存
volatile读
LoadStore屏障 ← 禁止重排序
普通读
普通读
法则3️⃣:锁规则(Monitor Lock Rule)
规则: 对锁的解锁,happens-before后续对该锁的加锁。
public class LockExample {
private int a = 0;
// 线程1
public synchronized void method1() {
a = 1; // ①
} // ② 释放锁
// 线程2
public synchronized void method2() { // ③ 获取锁
System.out.println(a); // ④ 一定看到a=1
}
}
保证:
- ② happens-before ③(锁规则)
- ① 在② 之前(程序顺序)
- ④ 在③ 之后(程序顺序)
- 所以:① happens-before ④
可视化:
线程1: [获锁] → a=1 → [释放锁]
↓ happens-before
线程2: [获锁] → 读a
生活类比:
公共厕所🚽:
- 线程1:进去(加锁)→ 在墙上写"到此一游"(a=1)→ 出来(解锁)
- 线程2:进去(加锁)→ 一定能看到墙上的字(读a)
法则4️⃣:线程启动规则(Thread Start Rule)
规则: Thread.start()之前的操作,happens-before 该线程的所有操作。
public class ThreadStartExample {
private int a = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println(a); // ③ 一定看到a=42
});
a = 42; // ①
t.start(); // ② happens-before ③
}
}
可视化:
主线程: a=42 → t.start()
↓ happens-before
新线程: 执行run()方法
法则5️⃣:线程终止规则(Thread Termination Rule)
规则: 线程的所有操作,happens-before 其他线程检测到该线程终止。
public class ThreadJoinExample {
private static int a = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
a = 42; // ①
}); // ② 线程结束
t.start();
t.join(); // ③ 等待线程结束
System.out.println(a); // ④ 一定输出42
}
}
保证:
- ① happens-before ②(线程内所有操作完成)
- ② happens-before ③(join返回)
- 所以:① happens-before ④
法则6️⃣:线程中断规则(Thread Interruption Rule)
规则: 对线程的interrupt()调用,happens-before 被中断线程检测到中断。
public class InterruptExample {
private static int a = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!Thread.interrupted()) { // ③ 检测中断
// ...
}
System.out.println(a); // ④ 一定看到a=42
});
a = 42; // ①
t.start();
t.interrupt(); // ② 中断
}
}
法则7️⃣:对象终结规则(Finalizer Rule)
规则: 对象的构造函数结束,happens-before 该对象的finalize()方法开始。
public class FinalizerExample {
private int a;
public FinalizerExample() {
a = 42; // ① 构造函数
} // ② 构造结束
@Override
protected void finalize() { // ③ finalize开始
System.out.println(a); // ④ 一定看到a=42
}
}
注意: finalize已被废弃,了解即可。
法则8️⃣:传递性规则(Transitivity)
规则: 如果A happens-before B,B happens-before C,则A happens-before C。
public class TransitivityExample {
private int a = 0;
private volatile boolean flag = false;
// 线程1
public void method1() {
a = 1; // ① A
flag = true; // ② B (volatile写)
}
// 线程2
public void method2() {
if (flag) { // ③ C (volatile读)
int b = a; // ④ D
}
}
}
推导:
- ① happens-before ②(程序顺序)
- ② happens-before ③(volatile规则)
- ③ happens-before ④(程序顺序)
- 传递: ① happens-before ④
四、实战案例:双重检查锁定(DCL)🔐
错误版本(有Bug)
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // ① 第一次检查
synchronized (Singleton.class) { // ② 加锁
if (instance == null) { // ③ 第二次检查
instance = new Singleton(); // ④ 创建对象
}
}
}
return instance; // ⑤
}
}
问题:
new Singleton()在字节码层面分三步:
- 分配内存
- 初始化对象
- 引用指向内存
可能重排序为:
- 分配内存
- 引用指向内存(instance不为null了!)
- 初始化对象
时序图:
时刻1: 线程A执行new,重排序后先让instance!=null
时刻2: 线程B检查instance!=null,直接返回
时刻3: 线程B使用instance(但还没初始化!)💥
正确版本(加volatile)
public class Singleton {
private static volatile Singleton instance; // volatile!
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
为什么加volatile就对了?
volatile禁止重排序,保证:
- 分配内存
- 初始化对象
- 引用指向内存(按顺序!)
线程B读取instance时,能看到完整初始化的对象!
五、as-if-serial:单线程的假象🎭
规则: 不管怎么重排序,单线程的执行结果不能变。
int a = 1; // ①
int b = 2; // ②
int c = a + b; // ③
可能的执行顺序:
- ① → ② → ③ ✅
- ② → ① → ③ ✅ (②和①无依赖)
- ① → ③ → ② ❌ (③依赖①和②)
数据依赖:
真依赖:先写后读
a = 1;
b = a + 1; // 依赖a
反依赖:先读后写
b = a + 1;
a = 2; // 不能排到前面
输出依赖:先写后写
a = 1;
a = 2; // 不能交换
六、内存屏障:happens-before的底层实现🔧
四种内存屏障
-
LoadLoad屏障
Load1 LoadLoad屏障 Load2保证:Load1先于Load2执行
-
StoreStore屏障
Store1 StoreStore屏障 Store2保证:Store1的数据在Store2之前刷新到内存
-
LoadStore屏障
Load1 LoadStore屏障 Store2保证:Load1先于Store2执行
-
StoreLoad屏障
Store1 StoreLoad屏障 Load2保证:Store1的数据对Load2可见(最昂贵!)
volatile的内存屏障
// volatile写
普通写;
StoreStore屏障; // 前面的写不能重排到volatile写后面
volatile写;
StoreLoad屏障; // volatile写不能和后面的读写重排
// volatile读
LoadLoad屏障; // 后面的读不能重排到volatile读前面
volatile读;
LoadStore屏障; // volatile读不能和后面的写重排
普通读;
七、实战:高性能计数器⚡
版本1:synchronized(安全但慢)
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int get() {
return count;
}
}
版本2:volatile(错误!)
public class Counter {
private volatile int count = 0; // ❌ volatile不保证原子性
public void increment() {
count++; // 三个操作:读、加、写,不是原子的!
}
public int get() {
return count;
}
}
问题:
线程A: 读count=0
线程B: 读count=0
线程A: count=1,写回
线程B: count=1,写回(覆盖了A的写入!)
结果:丢失一次更新
版本3:AtomicInteger(推荐)
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // CAS原子操作
}
public int get() {
return count.get();
}
}
版本4:LongAdder(高并发)
public class Counter {
private LongAdder count = new LongAdder();
public void increment() {
count.increment(); // 分段累加,减少竞争
}
public long get() {
return count.sum();
}
}
八、常见误区与陷阱⚠️
误区1:以为volatile就是线程安全
// ❌ 错误
private volatile int count = 0;
public void increment() {
count++; // 不是原子的!
}
// ✅ 正确:只用于状态标志
private volatile boolean flag = false;
public void setFlag() {
flag = true; // 单次赋值,OK
}
误区2:以为有锁就不用volatile
public class Example {
private boolean flag = false; // 没volatile
public synchronized void setFlag() {
flag = true;
}
public boolean getFlag() { // 没加锁!
return flag; // ❌ 可能看不到更新
}
}
修复:
- 方案1:读也加锁
- 方案2:flag加volatile
误区3:忽略对象发布的安全性
public class UnsafePublish {
private Data data;
public void init() {
data = new Data(); // ① 创建对象
data.setValue(42); // ② 初始化
}
public Data getData() {
return data; // ③ 其他线程可能看到未初始化的data!
}
}
修复:
private volatile Data data; // 加volatile
九、面试高频问答💯
Q1: happens-before和先后顺序是一回事吗?
A: 不是!
- happens-before:一种可见性保证
- 时间顺序:实际执行的先后
可能A happens-before B,但实际B先执行(只要结果正确)!
Q2: volatile能替代锁吗?
A: 部分场景可以:
- ✅ 状态标志(boolean flag)
- ✅ 一写多读的场景
- ❌ 复合操作(count++)
- ❌ 多个变量的一致性
Q3: 为什么需要happens-before?
A: 因为现代计算机有:
- 编译器优化(重排序)
- CPU乱序执行
- 多级缓存(不一致)
happens-before定义了程序员和JVM的契约!
Q4: final变量有happens-before保证吗?
A: 有!
- 对象构造完成 happens-before final字段对其他线程可见
- 所以final字段不需要volatile也能安全发布
十、总结:happens-before速查表📋
| 场景 | happens-before关系 | 用途 |
|---|---|---|
| 程序顺序 | ① → ② | 单线程内有序 |
| volatile | 写 → 读 | 状态同步 |
| 锁 | 解锁 → 加锁 | 临界区保护 |
| 线程启动 | start() → run() | 初始化传递 |
| 线程终止 | run()结束 → join() | 结果获取 |
| 中断 | interrupt() → 检测 | 中断响应 |
| 终结器 | 构造 → finalize | 资源清理 |
| 传递性 | A→B, B→C ⇒ A→C | 链式推导 |
记忆口诀:
程序顺序单线程,volatile锁保同步。 启动终止和中断,传递关系可推导。
下期预告: wait/notify为什么必须在synchronized中?Monitor锁的秘密!🔐