3.2 重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
3.2.1 数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。
数据依赖性分为以下三种:
- 写后读
- 写后写
- 读后写
这里所说的数据依赖性仅针对于单个处理器中执行的指令序列和单个线程中执行的操作。
3.2.2 as-if-serial语义
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进行了重排序,破坏了多线程程序的语义。