重排序定义
在前面我们提到过,重排序是编译器和处理器为了优化程序性能而对指令序列重新排序的一种手段。但是我们也知道代码不可能毫无原则的进行重排序,如果是毫无原则的进行重排序,那么我们的代码将无法获得预期的结果。因此重排序必须满足如下原则:
- 在单线程中不改变运行结果
- 操作不具备数据依赖性
那这两条原则如何理解么,我们先来看看下面的定义
数据依赖性
数据依赖性的意思是,若果两个操作访问同一个变量,并且其中一个操作是写操作,那么这两个操作就具备数据依赖性。
as-if-serial语义
as-if-serial语义的意思是,不管如何重排序,在单线程中,程序的执行结果不能被改变。编译器、处理器和runtime都不行遵守as-if-serial语义。注意:as-if-serial只对单线程有效,对多线程无效。
了解了数据依赖性和as-if-serial后,我们看看如下示例:
int a = 1; // A
int b = 2; // B
int c = a * b; // C
在上述示例中,我们根据数据依赖性可以分析出,A和C具备数据依赖性,B和C具备依数据赖性,A和B之前不存在数据依赖性关系,因此编译器和处理器在进行重排序时,A和B可以进行重排序,但是A和C,以及B和C是不能进行重排序的。因为尽管A和B进行了重排序,但是他们不影响程序在单线程中运行的结果。
在这里,我们联想前面讲到过的happens-before,可以看出上面代码存在3个happens-before关系。它们分别是:
- A happens-before B
- B happens-before C
- A happens-before C
在这里LZ再次强调,happens-before与时间上的先后完全没有关系,happens-before仅仅要求,前一个操作的结果对后一个操作可见。在这里操作A的结果不需要对B可见,重排序操作A和操作B的结果与按照A happens-before B 的执行结果一致。在这种情况下JMM认为这种重排序并不非法,JMM允许这种重排序。
重排序对多线程的影响
在了解了重排序的概率和数据依赖性以及as-if-serial的定以后,我们知道重排序在单线程中是没有影响的,那么重排序对多线程是否有影响呢?我么首先来看看下面的代码示例:
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线程执行write()方法,B线程执行reader()方法,那么在线程B在执行操作4时,能否获取到a=1呢?答案是不一定。
由于操作1 和操作2没有数据依赖性,因此编译器和处理器能够对这2个操作进行重排序,当操作1和操作2进行重排序时,此时程序的执行时序图是这样子的
如上图所示,操作1和操作2进行了重排序。程序在执行时,线程A首先写标记变量flag,随后线程B读取这变量,由于条件判断为真,线程B将读取变量a,此时变量a还没有被先A写入,在这里多线程程序的语义被重排序破坏掉了。
同样,操作3和操作4也可以进行重排序,但是操作3和操作4存在控制依赖的关系,即当操作3为真时,操作4才可以执行。当代码中存在控制依赖关系时,会影响指令序列执行的并行度。为此,编译器和处理器采用猜测执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前计算a*a,然后把计算的结果保存到重排序缓存中,当操作3的结果为真时,就把改计算结果写入变量i中。
通过上面的分析,我可以得出结论:重排序对单线程的执行结果没有影响,但是在多线程中,重排序可能会改变程序执行的结果。