重排序是值编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
- 在单线程环境下不能改变程序运行的结果;
- 存在数据依赖关系的不允许重排序
其实这两点可以归结于一点:无法通过happens-before原则推导出来的,JMM允许任意的排序。
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖性分为以下三种类型
名称 | 代码 | 说明 |
---|---|---|
写后读 | a=1;b=a; | 先写一个变量之后,再读这个变量 |
写后写 | a=1;a=2 | 先写这个变量之后,再写这个变量 |
读后写 | a=b;b=1; | 先读这个变量之后,再写这个变量 |
在上面3种情况,只要重排序两个操作的执行顺序,程序的执行结果就会发生改变。
编译器和处理器在重排序时,会遵循数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序。
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
A、B、C三个操作存在如下关系:A、B不存在数据依赖关系,A和C、B和C存在数据依赖关系,因此在进行重排序的时候,A、B可以随意排序,但是必须位于C的前面,执行顺序可以是A --> B --> C或者B --> A --> C。但是无论是何种执行顺序最终的结果C总是等于3。 as-if-serail语义把单线程程序保护起来了,它可以保证在重排序的前提下程序的最终结果始终都是一致的。
程序顺序规则
其实上段代码,他们存在这样的happen-before关系:
- A happens-before B
- B happens-before C
- A happens-before C
1、2是程序顺序次序规则,3是传递性。但是,不是说通过重排序,B可能会排在A之前执行么,为何还会存在存在A happens-beforeB呢?这里再次申明A happens-before B不是A一定会在B之前执行,而是A的对B可见,但是相对于这个程序A的执行结果不需要对B可见,且他们重排序后不会影响结果,所以JMM允许这种重排序。 我们需要明白这点,软件技术和硬件技术都有一个共同的目标:在不改变程序执行结果的前提下,尽可能并行度。
重排序对多线程的影响
在单线程环境下由于as-if-serial语义,重排序无法影响最终的结果,但是对于多线程环境呢?
举个例子
int a = 0;
boolean flag = false;
/**
* A线程执行
*/
public void writer(){
a = 1; // 1
flag = true; // 2
}
/**
* B线程执行
*/
public void read(){
if(flag){ // 3
int i = a + a; // 4
...
}
}
A线程执行writer(),线程B执行read(),线程B在执行时能否读到 a = 1 呢?
答案是:不一定
由于操作1 和操作2 之间没有数据依赖性,所以可以进行重排序处理,操作3 和操作4 之间也没有数据依赖性,他们亦可以进行重排序,但是操作3 和操作4 之间存在控制依赖性。
假如操作1和操作2之间重排序:
按照这种执行顺序线程B肯定读不到线程A设置的a值,在这里多线程的语义就已经被重排序破坏了。
因为在程序执行时,线程A首先写标记变量flag,随后B读这个变量。由于条件判断为真,线程B将读取变量a,此时变量a还没有被线程A写入。
假如操作3和操作4之间重排序:
在程序中,操作3和操作4存在控制依赖关系。当代码存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响,这里的"temp=a*a;"就是编译器和处理器采用的猜测执行,然后把计算结果临时保存到一个名为重排序缓存的硬件缓存中,当操作3的条件判断为真时,就把该计算结果写入变量i中。
猜测执行实质上对操作3和操作4做了重排序。重排序在这里破坏了多线程的语义。
重排序的影响
重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。
评论区