并发编程-重排序
目录重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
一、数据依赖性[相关]
若两个操作访问同一变量,且有一个为写操作,此时产生数据依赖性。数据以来分为下列 3 中类型:
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a = 1; b = a; | 先写后读 |
写后写 | a = 1; a = 2; | 先写后写 |
读后写 | a = b; b = 1; | 先读后写 |
上述三种情况,只要重排序两个操作的执行顺序,程序的执行结果都将被改变。
一般情况,编译器和处理器可能会对操作进行重排序,但若存在数据依赖关系,就不会进行重排序。
二、as-if-serial 语义
as-if-serial 语义:不管怎么重排序,程序的执行结果不能被改变。编译器、runtime、处理器都必须遵守 as-if-serial 语义。
为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执行结果。若操作间不存在数据依赖关系,这些操作九可能被编译器和处理器重排序。
例如:
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
上边 3 个操作的数据依赖关系如图:
如图所示,A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。因此在最终执行的指令序列中,C 不能重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,执行结果也将改变)。但 A 和 B 之间不存在数据依赖关系,编译器和处理器可以重排序 A 和 B 之间的执行顺序。
下图是该程序的两种可能会出现的执行顺序:
as-if-serial 语义把单线程程序保护了起来,遵守 as-if-serial 语义的编译器、runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉;单线程程序是按程序的顺序来执行的。as-if-serial 语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
三、程序顺序规则
根据 happens-before 的程序顺序规则,上面计算圆的面积的示例代码存在 3 个 happens-before 关系。
- A happens-before B
- B happens-before C
- A happens-before C
这里的第三个 happens-before 关系,是根据 happens-before 的传递性推导出来的。
这里 A happens-before B,但实际执行时,B 可以排在 A 之前执行。如果 A happens-before B,JMM 并不要求 A 一定要在 B 之前执行。JMM 仅仅要求前一个操作(执行的结果)堆后一个操作可见,且前一个操作按顺序排在第二个操作之前。这里操作 A 的执行结果不需要对操作 B 可见;而且冲排序操作 A 和操作 B 后的执行结果,与操作 A 和操作 B 按 happens-before 顺序执行的结果一致。这种情况下,JMM 会认为这种冲排序并不非法,JMM 允许这种重排序。
四、重排序对多线程的影响
现在看一个示例,验证重排序是否会改变多线程程序的执行结果。
class RecordExample{
int a = 0;
boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void reader(){
if (flag){
int i = a * a;
}
}
}