并发模型分类

并发编程我们需要处理两个问题:

  • 线程之间如何通信
    • 共享内存
    • 消息传递
  • 线程之间如何同步

Java 的并发采用的是共享内存模型。

Java 内存模型抽象

java 中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享。

局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java 线程之间的通信由 Java 内存模型(JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。JMM 抽象出来了每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

JMM模型

重排序

重排序有三种类型:

  • 编译器优化重排序
  • 指令重排序
  • 内存系统重排序

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

源代码 -》 编译器重排序 -》 指令重排序 -》 内存重排序 -》 最终指令序列

重排序可能导致多线程出现内存可见性问题。

数据依赖性

如果两个操作访问同一个变量,并且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。

名称 代码示例 说明
写后读 a=1; b=a; -
写后写 a=1; a=2; -
读后写 a=b; b=1; -

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。

as-if-serial

as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度), (单线程)程序的执行结果不能被改变。

为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。

如果操作之间不存在数据依赖关系,这些操作就可以被编译器和处理器重排序。

例如:下面代码 A 和 C 存在数据依赖, B 和 C 存在数据依赖,所以 C 不能重排序到 A 和 B 前面,但是 A 和 B 没数据依赖关系,可以重排序 A 和 B 之间的执行顺序。

1
2
3
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C

重排序对多线程的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a * a; //4
}
}
}

这里假设有两个线程 A 和 B, A 首先执行 writer()方法,随后 B 线程接着执行 reader()方法。

由于操作 1 和操作 2 没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作 3 和操作 4 没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。

操作 1 和操作 2 做了重排序。程序执行时,线程 A 首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量 a。此时, 变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

当操作 3 和操作 4 重排序时。在程序中,操作 3 和操作 4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执 行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程 B 的处 理器可以提前读取并计算 a*a,然后把计算结果临时保存到一个名为重排序缓冲 (reorder buffer ROB)的硬件缓存中。当接下来操作 3 的条件判断为真时,就把该计算结果写入变量 i 中。

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是 as- if-serial 语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中, 对存在控制依赖的操作重排序,可能会改变程序的执行结果。

happens-before

在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

happens-before 规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器的解锁,happens-before 于随后对这个监视器的加锁。
  • volatile 变量规则:对于一个 volatile 变量的写,happens-before 于任意后续对这个 volatile 变量的读。
  • 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。

程序顺序规则

根据 happens-before 的程序顺序规则,上面计算圆面积的例子存在3个 happens-before 关系:

  1. A happens-before B
  2. B happens-before C
  3. A happens-before C(传递性推导出来)

这里 A happens-before B,但是实际执行时候 B 可以排在 A 前面执行,因为 JMM 仅仅要求前一个操作对后一个操作可见,上面例子的 A 操作结果不需要对 B 操作可见;而且重排序操作 A 和操作 B 后的结果与顺序执行结果一致。在这种情况下, JMM 会认为这种重排序可以。

【参考资料】

  1. 深入理解java内存模型

—EOF—