核心概念
happens-before原则 是Java内存模型(JMM)中的核心规则,用于定义两个操作之间的内存可见性关系。如果操作A happens-before 操作B,那么A的执行结果对B可见,且A的执行顺序在B之前。
简单来说:前一个操作的结果对后续操作可见。
核心规则
JMM定义了以下8条happens-before规则:
1. 程序顺序规则(Program Order Rule)
在一个线程内,按照代码顺序,前面的操作happens-before后面的操作。
int a = 1; // 操作1
int b = 2; // 操作2
int c = a + b; // 操作3
// 操作1 happens-before 操作2 happens-before 操作3
注意:这不意味着前面的操作一定先执行,但前面操作的结果对后面操作可见。
2. 监视器锁规则(Monitor Lock Rule)
对一个锁的unlock操作 happens-before 后续对这个锁的lock操作。
public class LockExample {
private int value = 0;
private final Object lock = new Object();
public void writer() {
synchronized (lock) {
value = 1; // 操作1
} // unlock happens-before 后续的lock
}
public void reader() {
synchronized (lock) { // lock
int temp = value; // 操作2:能看到value=1
}
}
}
3. volatile变量规则(Volatile Variable Rule)
对一个volatile变量的写操作 happens-before 后续对这个变量的读操作。
public class VolatileExample {
private volatile boolean flag = false;
private int value = 0;
public void writer() {
value = 1; // 操作1
flag = true; // 操作2:volatile写
}
public void reader() {
if (flag) { // 操作3:volatile读
int temp = value; // 操作4:能看到value=1
}
}
}
// 操作2 happens-before 操作3,结合程序顺序规则,操作1的结果对操作4可见
4. 线程启动规则(Thread Start Rule)
线程的start()方法 happens-before 该线程的所有操作。
int x = 0;
Thread t = new Thread(() -> {
int y = x; // 能看到x=10
});
x = 10; // 操作1
t.start(); // 操作2 happens-before 线程内的操作
5. 线程终止规则(Thread Termination Rule)
线程的所有操作 happens-before 其他线程检测到该线程终止(通过join()或isAlive())。
int x = 0;
Thread t = new Thread(() -> {
x = 10; // 操作1
});
t.start();
t.join(); // 等待线程结束
int y = x; // 能看到x=10
6. 线程中断规则(Thread Interruption Rule)
对线程interrupt()方法的调用 happens-before 被中断线程检测到中断事件的发生。
Thread t = new Thread(() -> {
while (!Thread.interrupted()) {
// 能检测到中断
}
});
t.start();
t.interrupt(); // happens-before 线程内的interrupted()检测
7. 对象终结规则(Finalizer Rule)
对象的构造函数完成 happens-before 该对象的finalize()方法开始。
public class FinalizeExample {
private int value;
public FinalizeExample() {
value = 10; // happens-before finalize()
}
@Override
protected void finalize() {
int temp = value; // 能看到value=10
}
}
8. 传递性规则(Transitivity)
如果A happens-before B,B happens-before C,那么A happens-before C。
private volatile boolean flag = false;
private int value = 0;
// 线程1
value = 1; // A
flag = true; // B (volatile写)
// 线程2
if (flag) { // C (volatile读)
int x = value; // D,能看到value=1
}
// A happens-before B (程序顺序规则)
// B happens-before C (volatile规则)
// 因此 A happens-before C,再结合程序顺序规则,A happens-before D
原理分析
1. 与内存屏障的关系
happens-before规则在底层通过内存屏障(Memory Barrier) 实现:
- LoadLoad屏障:禁止读操作重排序
- StoreStore屏障:禁止写操作重排序
- LoadStore屏障:禁止读后写重排序
- StoreLoad屏障:禁止写后读重排序(开销最大)
// volatile写操作会插入屏障
value = 1;
// StoreStore屏障
flag = true; // volatile写
// StoreLoad屏障
// volatile读操作会插入屏障
if (flag) { // volatile读
// LoadLoad屏障
// LoadStore屏障
int x = value;
}
2. 重排序限制
happens-before规则限制了编译器和处理器的重排序行为:
- as-if-serial:在单线程中,重排序不能改变程序结果
- happens-before:在多线程中,保证必要的可见性和有序性
并发场景实践
1. DCL单例模式
public class Singleton {
// 必须使用volatile,防止指令重排序
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
// new操作分三步:
// 1. 分配内存
// 2. 初始化对象
// 3. 引用指向内存
// volatile防止2和3重排序
instance = new Singleton();
}
}
}
return instance;
}
}
2. 生产者-消费者模式
public class ProducerConsumer {
private volatile boolean hasData = false;
private int data = 0;
// 生产者
public void produce(int value) {
data = value; // 操作1
hasData = true; // 操作2:volatile写
}
// 消费者
public int consume() {
while (!hasData) { // volatile读
Thread.yield();
}
return data; // 能看到最新的data值
}
}
3. 线程安全的延迟初始化
public class LazyInitialization {
private volatile Helper helper;
public Helper getHelper() {
if (helper == null) {
synchronized (this) {
if (helper == null) {
// volatile保证初始化的完整性对其他线程可见
helper = new Helper();
}
}
}
return helper;
}
}
面试总结
核心要点
- happens-before是可见性保证,不是时间上的先后顺序
- 8大规则:程序顺序、锁、volatile、线程启动/终止/中断、对象终结、传递性
- 通过内存屏障实现:禁止特定的指令重排序
- 实际应用:DCL单例、线程通信、volatile变量等场景
答题模板
面试时可以这样回答:
- 定义:happens-before是JMM的核心规则,定义操作间的可见性关系
- 作用:如果A happens-before B,则A的结果对B可见,A在B之前
- 规则:列举2-3个常用规则(如volatile、锁、线程启动)
- 实践:举例说明(如DCL单例中volatile的作用)
- 原理:通过内存屏障限制重排序,保证多线程正确性
常见误区
// ❌ 错误理解:happens-before = 时间先后
int a = 1;
int b = 2;
// a=1不一定在时间上先于b=2执行,但a的结果对后续操作可见
// ✅ 正确理解:happens-before = 可见性保证
volatile boolean flag = false;
int value = 0;
// 写线程
value = 1;
flag = true; // volatile写
// 读线程
if (flag) {
int x = value; // 一定能看到value=1
}