happens-before 规则

本文详细解析了Java内存模型(JMM)的概念及其核心要素happens-before规则,探讨了这些规则如何确保多线程环境中内存访问的一致性和可见性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


1.happens-before 定义

happens-before的概念最初由Leslie Lamport在其一篇影响深远的论文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有兴趣的可以google一下。JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。具体的定义为:

1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

上面的1)是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!

上面的2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。

下面来比较一下as-if-serial和happens-before:

as-if-serial VS happens-before

1.  as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
2.  as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
3.  as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
具体规则 :

具体的一共有六项规则:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
    对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

总结

上面已经聊了关于JMM的两个方面:1. JMM的抽象结构(主内存和线程工作内存);2. 重排序以及happens-before规则。接下来,我们来做一个总结。从两个方面进行考虑。1. 如果让我们设计JMM应该从哪些方面考虑,也就是说JMM承担哪些功能;2. happens-before与JMM的关系;3. 由于JMM,多线程情况下可能会出现哪些问题?

JMM的设计

这里写图片描述
JMM层级图

JMM是语言级的内存模型,在我的理解中JMM处于中间层,包含了两个方面:(1)内存模型;(2)重排序以及happens-before规则。同时,为了禁止特定类型的重排序会对编译器和处理器指令序列加以控制。而上层会有基于JMM的关键字和J.U.C包下的一些具体类用来方便程序员能够迅速高效率的进行并发编程。站在JMM设计者的角度,在设计JMM时需要考虑两个关键因素:

程序员对内存模型的使用 程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。
编译器和处理器对内存模型的实现 编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型。
另外还要一个特别有意思的事情就是关于重排序问题,更简单的说,重排序可以分为两类:

会改变程序执行结果的重排序。
不会改变程序执行结果的重排序。
JMM对这两种不同性质的重排序,采取了不同的策略,如下。

对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种 重排序)
JMM的设计图为:
这里写图片描述
JMM设计示意图 从图可以看出:

JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不一定真实存在,比如上面的A happens-before B)。
JMM对编译器和处理器的束缚已经尽可能少。从上面的分析可以看出,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。例如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。再如,如果编译器经过细致的分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。

happens-before与JMM的关系

happens-before与JMM的关系
这里写图片描述
一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法


[1] : https://github.com/CL0610/Java-concurrency/tree/master/3.java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E4%BB%A5%E5%8F%8Ahappens-before%E8%A7%84%E5%88%99

<think>好的,我需要解释一下Java的happens-before规则。首先,我得回想一下这个规则的基本概念。happens-before是Java内存模型中的一个核心概念,用于确定多线程环境下操作的可见性和有序性。用户可能已经了解volatile关键字,但happens-before更广泛,涉及更多同步机制。 接下来,我需要明确happens-before的定义。它不是一个实际的时间顺序,而是一个保证,即如果一个操作happens-before另一个操作,那么第一个操作的结果对第二个操作是可见的。这涉及到编译器和处理器的重排序问题,需要确保在这些优化下,程序的执行结果符合预期。 然后,我应该列举具体的happens-before规则。比如程序顺序规则、锁规则、volatile变量规则、线程启动规则、线程终止规则、中断规则、终结器规则等。每个规则都需要简要说明,并举例子帮助理解。例如,程序顺序规则指的是同一线程中的每个操作happens-before后续操作,但实际执行时可能重排序,只要不影响结果。 用户可能想知道为什么需要这些规则,所以需要解释它们的作用,比如确保线程间的正确同步,避免数据竞争和内存可见性问题。同时,要对比其他语言如C++的处理方式,但这里集中在Java上。 还要注意happens-before与指令重排的关系,说明JVM如何通过这些规则限制重排序,保证多线程程序的正确性。可能需要提到内存屏障,但不用深入底层细节,保持解释在Java层面。 另外,用户可能有疑问,比如单线程下是否适用happens-before,需要说明单线程内遵循程序顺序,但多线程下需要显式同步机制来建立happens-before关系。 最后,通过一个例子来展示happens-before如何工作,比如两个线程通过synchronized块或volatile变量进行同步,如何确保可见性。同时,强调正确使用这些机制的重要性,否则可能导致难以调试的并发问题。 总结时,要重申happens-before的重要性,并建议在实际开发中如何应用这些规则,比如使用现有的同步工具而不是手动处理,避免错误。</think>--- ### Java 的 **happens-before 规则** 是内存模型(JMM)的核心概念,用于定义多线程环境下操作的 **可见性** 和 **有序性**。它并非字面意义上的“时间先后”,而是通过一组规则保证:**如果一个操作 A happens-before 操作 B,那么 A 的结果对 B 是可见的**,且编译器和处理器不会对这两个操作进行破坏规则的指令重排。 --- ### **1. happens-before 的六条基本规则** 以下是 Java 内存模型定义的 happens-before 规则: #### **(1) 程序顺序规则(Program Order Rule)** - **单线程内**,代码的书写顺序(程序顺序)决定了操作的执行顺序。 - **但允许指令重排**:只要重排不影响单线程的执行结果(即遵循 as-if-serial 语义)。 ```java int x = 1; // 操作 A int y = x + 2; // 操作 B:A happens-before B ``` --- #### **(2) 锁规则(Monitor Lock Rule)** - **解锁操作** happens-before **后续的加锁操作**(同一锁)。 - 保证锁释放前修改的变量对下一个加锁线程可见。 ```java synchronized (lock) { x = 42; // 操作 A(解锁时,A 的修改对其他线程可见) } // 解锁 happens-before 下一个加锁操作 synchronized (lock) { System.out.println(x); // 操作 B:读到 42 } ``` --- #### **(3) volatile 变量规则(Volatile Variable Rule)** - **对 volatile 变量的写操作** happens-before **后续对该变量的读操作**。 - 禁止指令重排,并强制刷新主内存。 ```java volatile boolean flag = false; // 线程 A flag = true; // 写操作 happens-before 线程 B 的读操作 // 线程 B if (flag) { // 一定能读到 true // 执行操作 } ``` --- #### **(4) 线程启动规则(Thread Start Rule)** - **线程的 `start()` 方法调用** happens-before **该线程的任何操作**。 ```java int x = 10; Thread t = new Thread(() -> { System.out.println(x); // 能读到 x = 10(父线程的修改对子线程可见) }); x = 20; // 修改在 start() 前,对子线程可见 t.start(); ``` --- #### **(5) 线程终止规则(Thread Termination Rule)** - **线程的所有操作** happens-before **其他线程检测到该线程终止**(如 `t.join()` 或 `t.isAlive()` 返回 false)。 ```java Thread t = new Thread(() -> { x = 42; }); t.start(); t.join(); System.out.println(x); // 保证读到 x = 42 ``` --- #### **(6) 中断规则(Interruption Rule)** - **对线程的 `interrupt()` 调用** happens-before **被中断线程检测到中断事件**(如抛出 `InterruptedException` 或调用 `isInterrupted()`)。 ```java Thread t = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { // 中断事件在 interrupt() 调用后触发 } }); t.start(); t.interrupt(); // happens-before 线程抛出异常 ``` --- ### **2. 传递性** 如果 **A happens-before B**,且 **B happens-before C**,则 **A happens-before C**。 这一特性使得跨线程的可见性得以传递。 ```java // 线程 A synchronized (lock) { x = 1; // 操作 A } // 解锁 happens-before 线程 B 加锁 // 线程 B synchronized (lock) { y = x; // 操作 B:读到 x = 1 } // 解锁 happens-before 线程 C 加锁 // 线程 C synchronized (lock) { z = y; // 操作 C:读到 y = 1 } ``` --- ### **3. happens-before 的实际意义** - **可见性**:确保一个线程的修改对其他线程可见。 - **有序性**:限制编译器和处理器的指令重排,避免破坏程序逻辑。 - **简化同步**:开发者无需关心底层内存细节,只需遵循规则设计同步机制。 --- ### **4. 示例分析** #### **错误代码(缺乏 happens-before 关系)** ```java int value = 0; boolean flag = false; // 线程 A value = 42; // 操作 1 flag = true; // 操作 2(非 volatile) // 线程 B while (!flag); // 操作 3 System.out.println(value); // 操作 4:可能输出 0 ``` - **问题**:线程 A 的 `flag` 不是 volatile,操作 1 和 2 可能被重排,且线程 B 可能看不到 `value` 的更新。 --- #### **修正代码(通过 volatile 建立 happens-before)** ```java volatile boolean flag = false; // 线程 A value = 42; // 操作 1 flag = true; // 操作 2(volatile 写) // 线程 B while (!flag); // 操作 3(volatile 读) System.out.println(value); // 操作 4:必输出 42 ``` - **解决**:volatile 写操作 2 happens-before volatile 读操作 3,且操作 1 不会被重排到操作 2 之后。 --- ### **5. 总结** - **happens-before 是逻辑关系**,而非物理时间顺序。 - **正确同步**(如锁、volatile、原子类)可以建立 happens-before,保证线程安全。 - **违反规则**会导致数据竞争和不可预测的结果。 **开发建议**:优先使用 `synchronized`、`volatile` 或 `java.util.concurrent` 工具类,而非手动控制内存可见性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值