java并发编程一一多线程线程安全(四)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34846877/article/details/82148954

##1.java重排序
###1.1数据依赖性
如果两个操作访问同一个变量时,且这两个操作汇中有一个为写操作,此时这两个操作之间就
存在数据依赖性。数据依赖分下列三种类型。

名称 代码示例 说明
写后读 a=1; b=a; 写一个变量之后,再读这个变量
写后写 a=1; a=2; 写一个变量之后,再写这个变量
读后写 a=b; b=1; 读一个变量之后,再写这个变量

上面这三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。
前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器做重排序时,
会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行
顺序。
注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的
操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理考虑。

###1.2as-if-serial语义

  1. as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)
    程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。
  2. 为了遵守as-if-serial语义编译器和处理器不会对数据依赖性关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖性,这些操作可能被编译器和处理器重排序。具体请看下面代码示例:
    double pi = 3.14; // A
    double r = 1.0; // B
    double area = pi * r *r; //C
    上面三个操作的数据依赖性如下图所示:
    这里写图片描述
    如上图所示,A和C之间存在数据依赖性,同时A和C之间也存在数据依赖性关系。因此在
    最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将
    会被改变)。但A和B之间没有数据依赖性关系,编译器和处理器可以重排序A和B之间的执行顺序。
    下图是该程序的两种执行顺序:
    这里写图片描述
    as-if-serial 语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为
    编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使
    单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

###1.3程序顺序规则
根据happens-before 的程序规则,上面计算圆的面积的实例代码存在三个happens-before关系:

  1. A happens-before B;
  2. B happens-before C;
  3. 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 后的执行结果,与操作A
    和操作B 案happens-before 顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法
    (not illegal),JMM允许这种重排序。
    在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度。
    编译器和处理器遵从这一目标,从happens-before的定义我们可以看出,JMM也同样遵从这一目标。

###1.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 对共享啊的
写入?
答案是不一定能看到。
由于操作1和操作2没有数据依赖性,编译器和出来器可以对着两个操作重排序;同样操作3和操作4
没有数据依赖性,编译器和处理器也可以对这两个操作重排序。让我们先来看看,当操作1和操作2
重排序时,可能会产生什么效果?情况下面的程序执行时序图:
这里写图片描述
如上图所示,操作1 和操作2 做了重排序。程序执行时,线程A首先先写标记变量flag,随后线程B读这个变量。
由于条件判断为真,线程B 将读取变量a。此时, 变量a 还根本没有被线程A写入,在这里多线程chengx
的语义就被重排序破坏了!

注意: 本文重中统一用红色的虚箭头表示错误的读操作,用绿色的虚箭头表示正确的读操作。

下面再看看,当操作3和操作4 重排序时会产生什么效果(借助这个重排序,可以顺便说明控制依赖性)
下面是操作3 和操作4 重排序后,程序的执行时序图:
这里写图片描述
在程序中,操作3 和操作4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行
的并行度。为此,编译器和处理器会采用猜测(speculation)执行来克服控制相关性对并行度的
影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果
临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。

从图中我们可以看出,猜测执行实质上对操作3 和操作4 做了重排序。重排序在这里破坏了多线程程序
的语义!在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义
允许对存在在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,
可能会改变程序的执行结果。

猜你喜欢

转载自blog.csdn.net/qq_34846877/article/details/82148954