重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段.

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性.数据依赖分为下列3种类型

名称 代码示例 说明
写后读 a=1;b=a; 写一个变量之后,再读这个位置
写后写 a=1;a=2; 写一个变量之后,再写这个变量
读后写 a=b;b=1; 读一个变量之后,再写这个变量

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

编译器和处理器可能会对操作做重排序.编译器和处理器的重排序会尊重数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序.

这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑

as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度).(单线程)的执行结果不能被改变.编译器,runtime和处理器都必须遵守as-if-serial语义,也就是说,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果.

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

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

如上 ,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系

所以:

  1. C不可能被重排序到AB之前
  2. A和B可以被重排序

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的.as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无须担心内存可见性问题.

程序顺序规则

根据happens-before的程序顺序规则,上面计算圆的代码存在3个happens-before关系

  1. A happens-before B
  2. B happens-before C
  3. 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会认为这种重排序不非法(not illegal),JMM允许这种重排序.

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能提高并行度.编译器和处理器遵从这一目标,从happens-before 的定义我们可以看出,JMM同样遵从.

重排序对多线程的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RecorderExample {
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首先执行writer() B接着执行reader()

线程B执行操作4的时候,能否看到A在对操作1对共享变量a的写入呢?

答案是: 不一定能看到

因为操作1和2 没有依赖关系

操作3 和4 没有依赖关系

如果多线程做了重排序

线程A 首先写标记flag

B读变量 然后读取变量a

最后A再写入a

多线程程序的语义被重排序破坏了

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但是在多线程中,对存在控制依赖的操作重排序,可能会改变程序的执行结果;