在不少状况下,访问一个程序变量(对象实例字段,类静态字段和数组元素)可能会使用不一样的顺序执行,而不是程序语义所指定的顺序执行。具体几种状况,以下:java
- 编译器 可以自由的以优化的名义去改变指令顺序;
- 在特定的环境下,处理器 可能会次序颠倒的执行指令;
- 数据可能在 寄存器、处理器缓冲区和主内存 中以不一样的次序移动,而不是按照程序指定的顺序;
例如,若是一个线程写入值到字段 a
,而后写入值到字段 b
,并且 b
的值不依赖于 a
的值,那么,处理器就可以自由的调整它们的执行顺序,并且缓冲区可以在 a
以前刷新 b
的值到主内存。有许多潜在的重排序的来源,例如编译器,JIT以及缓冲区。程序员
因此,从Java源码变成能够被机器(或虚拟机)识别执行的程序,至少要通过编译期和运行期。在这两个期间,重排序分为两类:编译器重排序、处理器重排序(乱序执行),分别对应编译时和运行时环境。因为重排序的存在,指令实际的执行顺序,并非源码中看到的顺序。编程
编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序,在不改变程序语义的前提下,尽量减小寄存器的读取、存储次数,充分复用寄存器的存储值。数组
假设第一条指令计算一个值赋给变量A并存放在寄存器中,第二条指令与A无关但须要占用寄存器(假设它将占用A所在的那个寄存器),第三条指令使用A的值且与第二条指令无关。那么若是按照顺序一致性模型,A在第一条指令执行事后被放入寄存器,在第二条指令执行时A再也不存在,第三条指令执行时A从新被读入寄存器,而这个过程当中,A的值没有发生变化。一般编译器都会交换第二和第三条指令的位置,这样第一条指令结束时A存在于寄存器中,接下来能够直接从寄存器中读取A的值,下降了重复读取的开销。缓存
另外一种编译器优化:在循环中读取变量的时候,为提升存取速度,编译器会先把变量读取到一个寄存器中;之后再取该变量值时,就直接从寄存器中取,不会再从内存中取值了。这样可以减小没必要要的访问内存。可是提升效率的同时,也引入了新问题。若是别的线程修改了内存中变量的值,那么因为寄存器中的变量值一直没有发生改变,颇有可能会致使循环不能结束。编译器进行代码优化,会提升程序的运行效率,可是也可能致使错误的结果。因此程序员须要防止编译器进行错误的优化。多线程
编译器和处理器可能会对操做作重排序,可是要遵照数据依赖关系,编译器和处理器不会改变存在数据依赖关系的两个操做的执行顺序。若是两个操做访问同一个变量,且这两个操做中有一个为写操做,此时这两个操做之间就存在数据依赖性。数据依赖分下列三种类型:并发
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a = 1;b = a; | 写一个变量以后,再读这个位置。 |
写后写 | a = 1;a = 2; | 写一个变量以后,再写这个变量。 |
读后写 | a = b;b = 1; | 读一个变量以后,再写这个变量。 |
上面三种状况,只要重排序两个操做的执行顺序,程序的执行结果将会被改变。像这种有直接依赖关系的操做,是不会进行重排序的。特别注意:这里说的依赖关系仅仅是在单个线程内。函数
举例:优化
class Demo {
int a = 0;
boolean flag = false;
public void write() {
a = 1; // 1
flag = true; // 2
}
public void read() {
if (flag) { // 3
int i = a * a; // 4
}
}
}
复制代码
因为操做 1 和 2 没有数据依赖关系,编译器和处理器能够对这两个操做重排序;操做 3 和操做 4 没有数据依赖关系,编译器和处理器也能够对这两个操做重排序。spa
当操做 1 和操做 2 重排序时,可能会产生什么效果?
如上图所示,操做 1 和操做 2 作了重排序。程序执行时,线程 A 首先写标记变量 flag,随后线程 B 读这个变量。因为条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!
当操做 3 和操做 4 重排序时,可能会产生什么效果?(借助这个重排序,能够顺便说明控制依赖性)
在程序中,操做 3 和操做 4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用 猜想(Speculation)执行 来克服控制相关性对并行度的影响。以处理器的猜想执行为例:
执行线程 B 的处理器能够提早读取并计算
a * a
,而后把计算结果临时保存到一个名为 重排序缓冲(reorder buffer ROB) 的硬件缓存中。当接下来操做 3 的条件判断为真时,就把该计算结果写入变量 i 中。
从图中咱们能够看出,猜想执行 实质上对操做3和4作了重排序。重排序在这里破坏了多线程程序的语义!
在单线程程序中,对存在控制依赖的操做重排序,不会改变执行结果(这也是
as-if-serial
语义容许对存在控制依赖的操做作重排序的缘由);在多线程程序中,对存在控制依赖的操做重排序,可能会改变程序的执行结果。
如今的CPU通常采用流水线来执行指令。一个指令的执行被分红:取指、译码、访存、执行、写回、等若干个阶段。而后,多条指令能够同时存在于流水线中,同时被执行。指令流水线并非串行的,并不会由于一个耗时很长的指令在“执行”阶段呆很长时间,而致使后续的指令都卡在“执行”以前的阶段上。相反,流水线是并行的,多个指令能够同时处于同一个阶段,只要CPU内部相应的处理部件未被占满便可。好比:CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段,而两条加法指令在“执行”阶段就只能串行工做。
然而,这样一来,乱序可能就产生了。好比:一条加法指令本来出如今一条除法指令的后面,可是因为除法的执行时间很长,在它执行完以前,加法可能先执行完了。再好比两条访存指令,可能因为第二条指令命中了cache而致使它先于第一条指令完成。通常状况下,指令乱序并非CPU在执行指令以前刻意去调整顺序。CPU老是顺序的去内存里面取指令,而后将其顺序的放入指令流水线。可是指令执行时的各类条件,指令与指令之间的相互影响,可能致使顺序放入流水线的指令,最终乱序执行完成。这就是所谓的“顺序流入,乱序流出”。
指令流水线除了在资源不足的状况下会卡住以外(如前所述的一个加法器应付两条加法指令的状况),指令之间的相关性也是致使流水线阻塞的重要缘由。CPU的乱序执行并非任意的乱序,而是以保证程序上下文因果关系为前提的。有了这个前提,CPU执行的正确性才有保证。
好比:
a++;
b=f(a);
c--;
复制代码
因为 b=f(a)
这条指令依赖于前一条指令 a++
的执行结果,因此 b=f(a)
将在 “执行” 阶段以前被阻塞,直到 a++
的执行结果被生成出来;而 c--
跟前面没有依赖,它可能在 b=f(a)
以前就能执行完。(注意,这里的 f(a)
并不表明一个以 a
为参数的函数调用,而是表明以 a
为操做数的指令。C语言的函数调用是须要若干条指令才能实现的,状况要更复杂些)。
像这样有依赖关系的指令若是挨得很近,后一条指令一定会由于等待前一条执行的结果,而在流水线中阻塞好久,占用流水线的资源。而编译器的重排序,做为编译优化的一种手段,则试图经过指令重排将这样的两条指令拉开距离,以致于后一条指令进入CPU的时候,前一条指令结果已经获得了,那么也就再也不须要阻塞等待了。好比,将指令重排序为:
a++;
c--;
b=f(a);
复制代码
相比于CPU指令的乱序,编译器的乱序才是真正对指令顺序作了调整。可是编译器的乱序也必须保证程序上下文的因果关系不发生改变。
因为重排序和乱序执行的存在,若是在并发编程中,没有作好共享数据的同步,很容易出现各类看似诡异的问题。