很多文章和书籍对于happens before这个概念的解释非常拗口,导致很多人,包括技术深厚的程序员也解释不清这个概念。
下面用小白能懂的语言解释一下“happens before”:
想象两个朋友在玩传话游戏
假设你和小伙伴A、B在玩传话游戏。
- 如果你(线程A)先告诉小伙伴A一句话,然后小伙伴A再转告给小伙伴B,那么你的话就会通过小伙伴A“传递”给B。这时候,你的话对B来说是“可见的”,而且顺序是确定的——你先说,A再说,B最后听到。
- 这种“传递”关系在Java多线程里就叫happens before,它确保一个操作的结果能被另一个操作看到,并且顺序正确。
具体场景举例
-
用
volatile
变量传话
如果你用一个“特殊纸条”(volatile
变量)写消息,写完之后,其他小伙伴(线程)立刻能看到纸条上的内容。- 例如:线程A把“今天下雨”写到
volatile
变量里,线程B马上就能读到这句话,不会因为缓存没刷新而读到旧消息。
- 例如:线程A把“今天下雨”写到
-
用锁传话
如果你和小伙伴A各有一把“密码锁”,你先解锁并修改了某个秘密,然后小伙伴A再解锁并读取,那么你的修改一定会被A看到。- 这就是锁的规则:解锁操作happens before加锁操作。
-
程序顺序规则
如果你先写日记本(变量a),再写另一篇日记(变量b),那么别人读的时候一定会先看到a的内容,再看到b的内容。即使电脑偷偷调整了顺序,只要符合逻辑,结果就正确。
为什么需要happens before?
电脑为了提高速度,可能会偷偷调整指令顺序(比如先写b再写a),或者让缓存里的旧数据“骗过”其他线程。
- happens before规则就是给程序员的一把“保护伞”,确保即使电脑偷偷优化,多线程程序也不会出错。
总结
happens before就像多线程世界的“传话协议”:
- 明确谁先传话、谁后听,避免信息错乱;
- 通过
volatile
、synchronized
等关键字实现,让并发代码更可靠。
上面的例子相信你已经理解了,下面我们再来深入学习一下
Java内存模型中的Happens-Before规则详解
一、Happens-Before规则的背景与意义
在并发编程中,多线程环境下数据的可见性、有序性和原子性问题一直是程序正确性的核心挑战。Java内存模型(JMM)通过定义Happens-Before规则,为这些问题提供了理论框架和解决方案。Happens-Before规则并非强制要求操作按时间顺序执行,而是通过逻辑上的因果关系,确保操作结果对其他线程的可见性。其核心目标是:只要操作A happens-before 操作B,则操作A的结果对操作B必然可见。
二、Happens-Before规则的具体内容
JMM共定义了8条Happens-Before规则,涵盖线程间、线程内及对象生命周期的交互场景。以下是关键规则的解析:
1. 程序顺序规则(Program Order Rule)
• 定义:在单个线程内,代码书写顺序决定了操作的先后顺序。例如,操作A在操作B之前书写,则A happens-before B。
• 作用:防止编译器对指令的重排序优化。即使处理器可能调整指令执行顺序,JMM仍通过此规则保证单线程内的逻辑一致性。
2. 锁的规则(Monitor Lock Rule)
• 定义:对同一锁的解锁(unlock)操作 happens-before 于后续对该锁的加锁(lock)操作。
• 示例:
synchronized (lock) {
sharedVar = 1; // 操作A(unlock前)
}
synchronized (lock) {
int temp = sharedVar; // 操作B(unlock后)
}
此时,操作A的结果对操作B可见。
3. Volatile变量规则
• 定义:对volatile变量的写操作 happens-before 后续对该变量的读操作。
• 原理:volatile写操作会插入StoreStore屏障和StoreLoad屏障,读操作插入LoadLoad屏障和LoadStore屏障,从而禁止指令重排序。
• 示例:
volatile boolean flag = false;
flag = true; // 写操作(happens-before)
if (flag) { // 读操作(可见true)
System.out.println("Flag is true!");
}
4. 线程启动与终止规则
• 启动规则:Thread对象的start()方法调用 happens-before 于新线程的任意操作。
• 终止规则:线程的所有操作 happens-before 其他线程通过join()或isAlive()检测到该线程终止。
• 示例:
Thread t = new Thread(() -> { /* 操作A */ });
t.start(); // 启动规则(happens-before)
t.join(); // 终止规则(可见操作A的结果)
5. 传递性规则(Transitivity)
• 定义:若A happens-before B,且B happens-before C,则A happens-before C。
• 重要性:通过传递性可推导复杂场景下的操作顺序。例如,锁的解锁→锁的加锁→volatile读操作,三者形成链式关系。
6. 其他规则
• 线程中断规则:interrupt()调用 happens-before 被中断线程检测到中断事件。
• 对象终结规则:对象构造函数完成 happens-before 其finalize()方法的开始。
三、Happens-Before与编译器优化、处理器重排序
JMM允许编译器和处理器对指令进行优化(如指令重排序),但必须遵守Happens-Before规则。例如:
• 单线程场景:编译器可能将无依赖关系的操作重排序,但程序顺序规则保证逻辑一致性。
• 多线程场景:通过内存屏障(Memory Barrier)和缓存一致性协议(如MESI)实现可见性。例如,volatile写操作后的StoreLoad屏障会强制将工作内存数据刷新至主内存,并使其他处理器的缓存失效。
关键点:Happens-Before规则通过限制特定类型的重排序(如破坏可见性的重排序),在性能与正确性间取得平衡。
四、Happens-Before的实际应用与示例
1. 双重检查锁定(DCL)的优化
class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 非原子操作
}
}
}
return instance;
}
}
• 分析:volatile关键字确保instance的写操作 happens-before 后续读操作,避免部分初始化问题。
2. 生产者-消费者模型
// 生产者
synchronized (buffer) {
buffer.put(item); // happens-before
buffer.notify(); // 通知消费者
}
// 消费者
synchronized (buffer) {
while (buffer.isEmpty()) {
buffer.wait(); // 等待生产者通知
}
item = buffer.take(); // happens-before
}
• 分析:锁的规则和等待/通知机制共同确保操作的有序性。
五、Happens-Before的局限性
- 无法覆盖所有场景:若两个操作之间无法通过已有规则推导出Happens-Before关系,则无法保证可见性。例如,两个独立线程的普通变量读写。
- 依赖程序员主动应用:规则本身不强制同步,需开发者通过synchronized、volatile等关键字显式实现。
六、总结与最佳实践
Happens-Before规则是Java并发编程的基石,其核心价值在于通过逻辑顺序约束实现内存可见性。开发者需:
- 明确操作间的依赖关系,合理选择同步机制(如锁、volatile)。
- 利用传递性简化复杂场景分析,例如通过锁和volatile组合实现跨线程同步。
- 避免过度优化,警惕指令重排序对并发程序的影响。
通过深入理解Happens-Before规则,开发者可编写出更高效、更安全的并发代码,充分发挥多核处理器的性能优势。
一句话总结:
happens-before规定某些操作必须在另一些操作之前“逻辑上”完成,确保结果对其他线程可见且顺序正确,即使它们实际执行顺序可能因编译器优化或处理器重排序被打乱。
关注我,获取更多技术干货与书单推荐~
顶级程序员都在偷偷看的书单!免费领50+本技术神作
关注公众号【苏师兄编程】,回复“书单”,即可领取上面书单