为了说明什么时候可以重排序,什么时候 不可以重排序,Java 引入了 JMM 模型 。该模型实际上就是一套规范,对上,是 JVM 和开发者之间的约定,对下,是 JVM 和编译器、CPU 之间的约定。

这套规范在开发者开发程序的方便性和系统运行的效率之间找到了一个平衡点。一方面,让编译器和 CPU 之间能够灵活地重排序,另一方面,要对开发者做一些承诺,明确告知开发者不需要感知什么样的重排序,需要感知什么样的重排序。然后,需要决定这些重排序是否对程序有影响。如果有影响,需要开发者显示的禁止重排序。

为了描述这个规范,JMM 引入了 happen-before 规则,使用 happen-before 描述来两个操作之间的内存可见性进而指令有序性。例如,A happen-before B,表示 A 的执行结果必须对 B 可见,也就是跨线程的内存可见性。

:::warning 注意,这里的可见性不是指 A 的操作必须在 B 的操作之前执行,而是指 A 操作的结果必须对 B 操作可见。 :::

happen-before 规则

基于 happen-before 规则,JMM 对开发者做出一系列承诺:

程序顺序执行规则(as-if-serial 规则)

在同一个线程中,有依赖关系的操作按照先后顺序,前一个操作必须先行发生于后一个操作(happens-before)。换句话说,单个线程中的代码顺序无论怎么重排序,对于结果来说是不变的。

volatile 变量规则

对 volatile 变量的写操作必须先行发生于对 volatile 变量的读操作。

基于 volatile 关键字的 happen-before 原则,罗列出 volatile 操作与前后指令之间可否重排序的清单:
image.png

  1. 最后一列可以看出:如果第二个操作为 volatile 写,无论第一个操作是什么都不能重排序,这就确保了 volatile 写之前的操作不会被重排序到自己之后
  2. 倒数第二行可以看出:如果第一个操作为 volatile 读,无论第二个操作是什么都不能重排序,这确保了 volatile 读之后的操作不会被重排序到自己的前面

:::info Java 中 volatile 关键字不仅具有内存可见性,还会禁止 volatile 变量与非 volatile 变量的写入重排序。但是,volatile 不能保证数据的原子性。 :::

传递性规则

若 A happen-before B,B happen-before C,那么 A happen-before C。来看下面的例子:

  1. public class HappenBeforeTest {
  2. private int a = 0;
  3. private volatile int c = 0;
  4. public void set() {
  5. a = 5; // ①
  6. c = 1; // ②
  7. }
  8. public int get() {
  9. int d = c; // ③
  10. return a; // ④
  11. }
  12. }

假设线程 A 先调用了 set,设置了 a = 5 之后线程 B 调用了 get,返回值一定是 a = 5。因为 ① 和 ② 是运行在统一线程中,③ 和 ④ 运行在同一线程中,因此 ① happen-before ② ,③ happen-before ④;又因为 c 是 volatile 变量,因此对 c 的写入 happen-before 对 c 的读取,所以 ② happen-before ③。根据 happen-before 规则的传递性得到: ① happen-before ② happen-before ③ happen-before ④

:::info 从前面的规则可以知道:如果第二个操作为 volatile 写,无论第一个操作是什么都不能重排序。拿上面的代码来说,由于代码 ② 为写入 c(volatile 变量)操作,因此代码 ① 不会被重排序到代码 ② 的后面。

从前面的规则可以知道:如果第一个操作为 volatile 读,无论第二个操作是什么都不能重排序。拿上面的代码来说,由于代码 ③ 为读取 c(volatile 变量),因此代码 ④ 不会被重排序到代码 ③ 之前 :::

synchronized 规则

对于 synchronized 的解锁操作必须先行发生与对 synchronized 的加锁操作。即无论在单线程还是多线程中,同一个锁如果处于被锁定状态,那么必须先对锁进行释放操作,后面才能继续执行 lock 操作。

线程 start 原则

对线程的 start 操作先行于这个线程内部的其他任何操作。具体来说,如果线程 A 执行 B.start() 启动线程 B,那么线程 A 的 B.start() 操作先行发生于线程 B 中的任意操作。反过来说,如果主线程 A 启动子线程 B 后,线程 B 能看到线程 A 在启动操作前的任何操作。

线程 join 原则

如果线程 A 执行了 B.join() 操作并成功返回,那么线程 B 中的任意操作先行发生于线程 A 所执行的 ThreadB.join() 操作。join() 规则和 start() 规则刚好相反,线程 A 等待子线程 B 完成后,当前线程 B 的赋值操作,线程 A 都能够看到。