16_03_Happen-Before原则的借助

Happen-Before原则的借助

Happen-Before 原则可以保证变量的可见性,因此可以通过 “借助” 的方式,来间接地实现对无锁保护变量的安全性保护。

“借助” 技术,一般将程序次序规则和其他某个规则(比如监视器锁规则或者 volatile 变量规则)结合起来实现的。

这种 “借助” 技术,对于代码语句的顺序非常敏感,因此很容易出错,属于一种高级技术。

举个例子来说明一下这个技术。

一、有并发问题的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class VolatileTest {

int x = 0;
boolean ready = false;

// 线程A,先执行
public void writer() {
x = 1;
ready = true;
}

// 线程B,后执行
public void reader() {
if (ready == true) {
// x的值是什么,0?1?
}
}
}

线程 B 读到的值可能是什么呢?

(1) 首先,编译出来的指令可能会重排序优化

那么,线程 A 执行的指令代码可能是这样的:

1
2
3
4
public void writer() {
ready = true;
x = 1;
}

也就是说,线程 A 可能先执行 ready = true,然后再执行 x = 1。

(2) 其次,线程 B 对于线程 A 的“影响”存在可见性问题

那么,线程 B 看到的情况可能有几种:

  1. 没看到 ready = true(可见性问题)
  2. 看到了 ready = true,但是没有看到 x = 1(有序性问题)
  3. 看到了 ready = true,也看到了 x = 1

理论上来说,只有第三种情况,才是正确的,但结果未必是这样的。

所以,由于存在有序性问题和可见性问题,线程 B 读到的 x 值是未知的。

二、“借助”技术的可见性保证

这里,只对上面例子中的 ready 加了 volatile 修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class VolatileTest {

int x = 0;
volatile boolean ready = false;

// 线程A,先执行
public void writer() {
x = 1;
ready = true;
}

// 线程B,后执行
public void reader() {
if (ready == true) {
// x的值是什么,0?1?
}
}
}

那么线程 B 看到的值都是什么?

(1) 程序次序规则

首先,volatile 变量具有禁止指令重排序的语义,可以保持有序性。

那么在线程 A 中,操作 x = 1 就只能在操作 ready = true 之前执行。

依据程序顺序规则,操作 x = 1 先于操作 ready = true

(2) volatile 变量规则

其次,对于 volatile 变量 ready,线程 A 写操作在线程 B 的读操作之前执行

依据 volatile 变量规则,操作 ready = true 也先于操作 ready == true

(3) 传递性规则

最后,依据传递性规则,操作 x = 1 必然先于操作 ready == true

所以,线程 B 执行到 ready == true 时,必然可以看到 x = 1

总结

通过间接地利用 Happen-Before 原则,可以实现无锁保护变量的可见性。

不过这种技术,需要很精确地控制操作的执行顺序,一不小心就很容易写错。

作者

jiaduo

发布于

2022-05-15

更新于

2023-04-03

许可协议