3.2重排序
重排序是指编译器和处理器为了优化性能而对指令序列进行重排序的一种手段。
3.2.1数据依赖性
如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖性分为3种:
- 写后读
- 写后写
- 读后写
编译器和处理器在重排序时,会遵守数据依赖性,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作。
3.2.2 as-if-serial语义
as-if-serial的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖性关系的操作进行重排序,因为有可能会改变执行结果。
3.2.3 程序顺序规则
在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能提高并行度。
3.2.4 重排序对多线程的影响
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
.....
}
}
}
flag变量是个标记,用来标识变量a是否写入。这里假设有两个线程A和B,A首先执行writer()方法。随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写如呢?
答案是:不一定
由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两操作重排序,同理操作3和4也可能会被重排序。当操作1和操作2重排序时,程序执行的时序图如下:
如图所示,操作1和操作2进行了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读这个变量。由于条件判断为真,线程B将读取变量a。此时变量a还未被线程A写入,在这里多线程的语义就被重排序破坏了!
下面我们再看看操作3和操作4重排序会产生什么效果?
在程序中,操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。猜测执行实质上对操作3和操作4进行了重排序,破坏了多线程程序的语义。
注意:在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。