happens-before原则:多线程的时间法则⏰

37 阅读9分钟

在多线程的世界里,时间不是线性的!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)定义的一套规则,告诉我们:

  1. 哪些操作是有序的(不会被重排序)
  2. 哪些写入是可见的(能被其他线程看到)

生活类比:

想象你给朋友发微信消息📱:

  • 没有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; // ③ 依赖①,不能排到①前面

实际执行可能是:②→①→③

生活类比:

你做早餐🍳:

  1. 煮咖啡(①)
  2. 烤面包(②)
  3. 涂果酱(③,依赖②)

①和②可以同时做(重排序),但③必须在②之后。


法则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会:

  1. 禁止之前的写操作重排序到volatile写之后
  2. 刷新缓存到主内存

volatile读取时,JMM会:

  1. 禁止之后的读操作重排序到volatile读之前
  2. 从主内存读取最新值

内存屏障:

写操作:
  普通写
  普通写
  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()在字节码层面分三步:

  1. 分配内存
  2. 初始化对象
  3. 引用指向内存

可能重排序为:

  1. 分配内存
  2. 引用指向内存(instance不为null了!)
  3. 初始化对象

时序图:

时刻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禁止重排序,保证:

  1. 分配内存
  2. 初始化对象
  3. 引用指向内存(按顺序!)

线程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的底层实现🔧

四种内存屏障

  1. LoadLoad屏障

    Load1
    LoadLoad屏障
    Load2
    

    保证:Load1先于Load2执行

  2. StoreStore屏障

    Store1
    StoreStore屏障
    Store2
    

    保证:Store1的数据在Store2之前刷新到内存

  3. LoadStore屏障

    Load1
    LoadStore屏障
    Store2
    

    保证:Load1先于Store2执行

  4. 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锁的秘密!🔐