多线程之重排序详解

重排序

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

数据依赖性

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

上面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与C之间存在数据依赖关系,所以C不能排到A的前面,同时B与C之间也存在数据依赖关系,所以,C也不能排到B的前面,但是A与B之间是不存在数据依赖关系的,所以A与B之间是可以进行重排序的。

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

程序顺序规则

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

 A happens-before B ; B  happens-before  C;  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按happens-before 顺序执行的结果是一致的。JMM会认为这种重排序并不是非法的,JMM允许这种重排序。

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

重排序对多线程的影响

首先看一下代码实例:

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
             ......
    }
} 
}

flag是一个变量,用来表示变量a是否已被写入。这里假设有两个线程A和B ,A线程首先执行writer方法,随后线程B执行reader方法。线程B在执行操作4的时候,能否看到线程A在操作共享变量a的写入呢?

答案是:在多线程的情况下,不一定能看到;

由于操作1和操作2没有数据依赖的关系,编译器和处理器可以对这两个操作进行重排序,操作3和操作4没有数据依赖关系,编译器和处理器也可以对其进行重排序,下面我们看一下可能的执行情况的示意图:

如上所示,操作1 和操作2 进行了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读这个变量。由于判断条件为真,线程B将读取变量a。此时,变量a还没有被线程A写入,所以在这里,多项层程序的语义就被重排序破坏了。

下面在看一下操作3和操作4重排序会发生什么效果:

在程序中,操作3和操作4存在控制依赖关系。当代码中存在控制依赖行时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲的硬件缓存中。当操作3的条件判断为真的时候,就把该结算结果写入到变量i中。

从上图我们可以看出,猜测执行实质上是对操作3和操作4进行了重排序,重排序在这里破坏了多线程程序的语义。

在单线程程序中,对存在控制依赖的操作进行重排序,不会改变执行结果(这也是as-if-serial 语义允许对存在控制依赖的操作做重排序的原因),但是在多线程的程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

猜你喜欢

转载自blog.csdn.net/IBLiplus/article/details/83817774