JUC之Java并发基础篇——指令重排与happens-before

​ 在执行程序时,为了提高性能,编译器和处理器会对指令做一些优化,即指令重排序。但是,重排序也要有一定的标准和依据,否则,就会出现程序不受控制,结果与预期不一致。所以,重排序一定要保证,在重排序后,程序的逻辑不发生改变。保证语义,有 as-if-serial ;保证内存可见性, Java 编程语言规范中,也有 happens-before 规则对其进行限制。

指令重排

经典案例

​ 先从一个经典案例来看下指令重排是否真的存在。

```
public class MyTest {private static int x = 0, y = 0;private static int a = 0, b =0;public static void main(String[] args) throws InterruptedException {int i = 0;while(true) {i++;x = 0; y = 0;a = 0; b = 0;Thread one = new Thread(() -> {a = 1;x = b;});Thread two = new Thread(() -> {b = 1;y = a;});one.start();two.start();one.join();two.join();String result = “第” + i + “次 (” + x + “,” + y + “)”;if(x == 0 && y == 0) {System.err.println(result);break;} else {System.out.println(result);}}}
}


考虑到多线程,如果指令不重排,那么输出结果只会出现以下三种:

> 第 \*\*\* 次 (0,1)
> 
> 第 \*\*\* 次 (1,1)
> 
> 第 \*\*\* 次 (1,0)

而实际中,出现了下面的结果

> 第4403次 (1,0)> 第4404次 (1,0)> 第4405次 (0,1)> 第4406次 (0,1)> 第4407次 (0,1)> 第4408次 (0,1)> 第4409次 (0,0)
> 
> …. 修改代码跑出 1,1 来 ….
> 
> 第1069712次 (1,1)

出现了 `0,0` 这个结果,说明出现了指令重排,否则,至少会出现一个 1 才对。

### [](about:blank#as-if-serial%E8%AF%AD%E4%B9%89 "#as-if-serial语义")as-if-serial语义

​ 前文有说,重排序要保证程序的逻辑不变性。具体点就是,单线程环境下,程序的行为和结果不能发生变化。这就是所谓的 `as-if-serial` 语义。而要保证这点,对于有依赖关系的前后操作,不能进行重排序。

int x = 1;
int y = 2;
int z = x + y;


​ 所以上述代码,一二行可以进行重排,因为二者不存在任何依赖关系。而第三行不能重排至一二行之前,因为第三行依赖于前两行的数据结果。

### [](about:blank#%E5%BC%82%E5%B8%B8%E6%83%85%E5%86%B5%E4%B8%8B%E7%9A%84%E9%87%8D%E6%8E%92%E5%BA%8F "#异常情况下的重排序")异常情况下的重排序

​ 那么,如果在满足 `as-if-serial` 的情况下进行了重排序,运行中发了异常呢?

public static void main(String[] args) {int x = 1;int y = 2;try {x = 3;y = 2 / 0;} catch (Exception e) {e.printStackTrace();} finally {System.out.println("x = " + x);}
}


​ `x` 和 `y` 并不存在依赖关系,所以可以进行指令重排,理论上打印结果会出现 `x = 1` 的情况,但是实没并不会这样。这是因为编译器在处理这种情况时,会在 `catch` 语句中插入补偿代码,以保证 `as-if-serial` 语义。

[](about:blank#happens-before "#happens-before")happens-before
--------------------------------------------------------------

​ 上文提到,指令重排要保证 `as-if-serial` 语义,但是只对单线程有效。那么,如果在多线程情况下,如果不加限制,那么工作内存和主内存再加上重排序,一定会出现各种乱七八糟的问题。所以一定要有一个规则,来保证内存可见性。从 `JDK5` 起, `JMM` 就完善了 `happens-before` 来强调两个操作的顺序。

### [](about:blank#%E5%90%AB%E4%B9%89 "#含义")含义

​ `happends-before` 不要理解为谁谁早于谁谁发生,不然你会一直弄不清这个规则说的到底是什么。`happens-before` 说的是两个操作的**可见性**。

​ 两个操作可以用 happens-before 来确定它们的执行顺序。如果一个操作 happens-before 于另一个操作,那么我们说第一个操作优先且对于第二个操作是**可见的**。

如果我们有 x 和 y 两个操作,我们用 **hb(x, y)** 来表示 **x happens-before y**

* 如果 x 和 y 是同一个线程的两个操作且在代码上 x 先于 y ,那么有 hb(x, y)
* 对象构造方法的最后一行指令 `happens-before` 于 `finalize()` 方法的第一行指令。
* 如果操作 x 与随后的操作 y 构成同步,那么 hb(x, y)。
* hb(x, y) 和 hb(y, z),那么可以推断出 hb(x, z)

> 参见: [The Java® Language Specification#Happens-before Order](https://link.juejin.cn/?target=https%3A%2F%2Fdocs.oracle.com%2Fjavase%2Fspecs%2Fjls%2Fse8%2Fhtml%2Fjls-17.html%23jls-17.4.5 "https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5")
> 
> 下面的具体也是翻译自此

### [](about:blank#%E5%85%B7%E4%BD%93%E8%A7%84%E5%88%99 "#具体规则")具体规则

​ 上面所说可以简化成以下列表:

#### [](about:blank#%E9%94%81%E8%A7%84%E5%88%99 "#锁规则")锁规则

对一个监视器的解锁操作 `happens-before` 于后续的对这个监视器的加锁操作

这条规则的意思是,解锁动作一定要让所有线程都知道,不然后续可能没办法加锁。

#### [](about:blank#volatile-%E8%A7%84%E5%88%99 "#volatile-规则")volatile 规则

对 `volatile` 属性的写操作 `happens-before` 于后续对这个属性的读操作

这条规则的意思是,`volatile` 属性在进行写操作之后,对应的值一定要对读可见,即读的一定是最新值。顺便一提,`volatile` 还有一个作用是禁止指令重排。

| 线程A | 线程B |
| --- | --- |
| 1,修改共享变量值 ||
| 2,写 volatile 变量 ||
|| 3,读 volatile 变量 |
|| 4,读共享变量值 |

在本条规则的限制下,3 和 4 读取的共享变量值一定是最新的。因为有hb(1,2),hb(2,3),hb(3,4),所以有hb(1,4)

#### [](about:blank#%E7%BA%BF%E7%A8%8B-start-%E8%A7%84%E5%88%99 "#线程-start-规则")线程 start 规则

如果 A 线程中调用了线程 B.start(),那么B.start() `happens-before` 于 B线程中所有操作

这条规则参见下面表格:

| 线程A | 线程B |
| --- | --- |
| 1,修改共享变量 ||
| 2,执行 B.start() ||
|| 3,执行对应逻辑 |
|| 4,读共享变量 |

此时,B 在操作 4 中读取的变量值也一定是最新的

#### [](about:blank#%E7%BA%BF%E7%A8%8B-join-%E8%A7%84%E5%88%99 "#线程-join-规则")线程 join 规则

如果 A 线程中调用了线程 B.join(),那么 B 线程中的操作 `happens-before` 于 A 线程调用B. join() 之后的任何语句

同样的,看下面的表格:

| 线程A | 线程B |
| --- | --- |
| 1,执行 B.join() | 2,写共享变量 |
|| 3, 执行完毕,终止 |
| 4,B.join()成功返回 ||
| 5,读共享变量 |

此时,A 在操作5中读取到的共享变量的值也一定是最新的

#### [](about:blank#%E5%88%9D%E5%A7%8B%E5%8C%96%E8%A7%84%E5%88%99 "#初始化规则")初始化规则

对象的默认值初始化 `happens-before` 于程序中对它的其他操作

这点也就是说,在调用了一个对象的构造器之后,一定会给所有的属性赋值默认值。基本类型赋值基本类型的默认值,对象赋值为 `null` ,这点早于其它所有操作

> 注:上面的表格里的推导,都用到了含义中讲到的这两点:
> 
> 程序次序规则:如果 x 和 y 是同一个线程的两个操作且在代码上 x 先于 y ,那么有 hb(x, y)。
> 
> 传递性规则:即,如果有 hb(x,y) 和 hb(y,z) 则有 hb(x,z)。
> 
> 而程序次序规则也不是说 x 一定要 y 之前执行,只是说可见,如果没有依赖,是可以重排序的。

[](about:blank#%E6%80%BB%E7%BB%93 "#总结")总结
------------------------------------------

* 指令重排是有条件的,要保证重排的前后两个操作,不存在依赖关系
* 指令重排只能保证重排后,对单线程而言,行为和结果不发生变化,并不保证多线程的安全
* 为了保证 `as-if-serial` ,异常时会有补偿逻辑,`catch` 中有错误补偿代码
* `happens-before` 强调的是**可见性**,而不是谁谁先于谁发生
* `A happens-before B` 指的是 **A 操作的结果对 B 可见**,如果两个操作不存在依赖,即 B 不需要知道 A 的结果,是可以进行指令重排序的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值