0
点赞
收藏
分享

微信扫一扫

重排序详解,按我认为的排序执行,嘿嘿!




  • j3_liuliang
  • 趁着上一篇的热度,接着再来说说重排序的问题


本篇主要是讨论一下内存模型中的重排序问题,根据Java内存模型中对重排序的内容划分主要是下面四个方面介绍;


  1. 数据依赖性(​​前置条件​​)
  2. as-if-serial语义(​​约束条件​​)
  3. 程序顺序规则(​​执行规则​​)
  4. 重排序对多线程的影响(​​最后结果​​)

内容不是很多(又在瞎说!)但不一定好消化,哈哈!毕竟是偏向理论性的东西,好消化才怪呢;不过我(j3)可以给你们提个意见,就是,关注我呀!看我​​后续文章​​,这一来二去的肯定把你们整的明明白白的;

那么前往知识的海洋,要开船哦!

重排序详解,按我认为的排序执行,嘿嘿!_java

一、数据依赖性

如果两个操作访问同一个变量,且这两个操作中​有一个为写操作​,此时这两个操作 之间就​存在数据依赖性​;

数据依赖分下列三种类型:

名称

代码示例

说明

写后读

a = 1;

b = a ;

写一个变量之后,再读这个位置。

写后写

a = 1;

a = 2 ;

写一个变量之后,再写这个变量。

读后写

a = b;

b = 1 ;

读一个变量之后,再写这个变量。

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变;

前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在​重排序​时,会​遵守数据依赖性​,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序;

注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑

二、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

上面三个操作的数据依赖关系如下图所示:

重排序详解,按我认为的排序执行,嘿嘿!_其他_02

如上图所示,A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系;

因此在最终执行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的结果将会被改变)。但 A 和 B 之间没有数据依赖关系,编译器和 处理器可以重排序 A 和 B 之间的执行顺序;

下图是该程序的两种执行顺序:

重排序详解,按我认为的排序执行,嘿嘿!_后端_03

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

三、程序顺序规则

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


  1. A happens- before B;
  2. B happens- before C;
  3. A happens- before C;

这里的第 3 个 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 允许这种重排序;

重排序详解,按我认为的排序执行,嘿嘿!_java_04

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

四、重排序对多线程的影响

现在让我们来看看,重排序是否会改变多线程程序的执行结果。请看下面的示例代码:

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 重排序时,可能会产生什么效果? 请看下面的程序执行时序图:

重排序详解,按我认为的排序执行,嘿嘿!_重排序_05

如上图所示,操作 1 和操作 2 做了重排序;

程序执行时,线程 A 首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量 a。此时, 变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

※​​:本文统一用红色的虚箭线标识错误的读操作,用绿色的虚箭线标识正确的读 操作;

操作 3 和操作 4 重排序

下面再让我们看看,当操作 3 和操作 4 重排序时会产生什么效果(借助这个重排 序,可以顺便说明​​控制依赖性​​)。下面是操作 3 和操作 4 重排序后,程序的执行时 序图:

重排序详解,按我认为的排序执行,嘿嘿!_后端_06

在程序中,操作 3 和操作 4 ​存在控制依赖关系​。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用​​猜测​​(Speculation)执行来克服控制相关性对并行度的影响;


存在控制依赖关系 =》影响指令序列的并行度 =》解决:编译器和处理器采用​​猜测执行​


以处理器的猜测执行为例,执行线程 B 的处 理器可以提前读取并计算 a*a,然后把计算结果临时保存到一个名为​​重排序缓冲​​ (reorder buffer ROB)的硬件缓存中。当接下来操作 3 的条件判断为真时,就把 该计算结果写入变量 i 中;

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

重排序详解,按我认为的排序执行,嘿嘿!_其他_07

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

结束语


  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。


举报

相关推荐

0 条评论