目录
1.了解计算机硬件哪些事儿
在前面章节《JVM之缓存行对齐》文章中介绍了缓存,那么体现在计算机硬件(架构设计)上,可以参照下面的图。由于计算机硬件上的差别(越往上的硬件越快,价格越贵,容量越小),导致计算机在设计的时候通常是软件和硬件相互补充,这种组织存储器系统的方法称为存储器层次结构。
2.指令重排场景再现
有了上面的大的背景,下面在以一个实际的例子说明下指令重排
说明:
- 程序就是指令,一条一条在CPU中进行执行的;
- 如果遇到耗时的操作,为了提高效率,按照一定的规则进行指令优化;比如上面指令2是耗时操作,那么CPU没必要都在这里等着,在符合原则的情况下,会优先去执行后面的指令3,这样整体上效率是最高的,这就是指令重排为什么出现了。
3.指令重排定义
编译器指令重排
通过调整代码中的指令顺序,在不改变代码语义的前提下,对变量访问进行优化。从而尽可能的减少对寄存器的读取和存储,并充分复用寄存器。但是编译器对数据的依赖关系判断只能在单执行流内,无法判断其他执行流对竞争数据的依赖管理。
CPU指令重排
流水线(Pipeline)和乱序执行是现代CPU基本都具有的特性。机器指令在流水线中经历取指、译码、执行、访存、写回等操作。为了CPU的执行效率,流水线都是并行处理的,在不影响语义的情况下。处理器次序(Process Ordering,机器指令在CPU实际执行时的顺序)和程序次序(Program Ordering,程序代码的逻辑执行顺序)是允许不一致的,即满足As-if-Serial特性。显然,这里的不影响语义依旧只能是保证指令间的显式因果关系,无法保证隐式因果关系。即无法保证语义上不相关但是在程序逻辑上相关的操作序列按序执行
指令重排定义
在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
4.数据依赖性
程序指令之间存在数据依赖的情况下指令的顺序是不允许进行交换的。例如:
名称 代码示例 说明
写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
上面写操作的位置是不允许变化的,否则将带来不一样的执行结果。编译器不会对存在数据依赖的程序指令进行重排,这里的依赖性仅仅指单线程情况下的数据依赖性,多线程并发情况下,此规则将失效。
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime、处理器都必须遵守as-if-serial语义。
5.指令重排实例演示
package test;
import java.util.ArrayList;
import java.util.concurrent.Semaphore;
public class DisorderTest {
int a = 0;
boolean flag = false;
public void writer(){
a =1;
flag = true;
ArrayList<String> test = new ArrayList<>();
}
public void reader(){
if(flag){
// System.out.println("执行中 ");
if(a == 0) {
System.out.println("==============发生了指令重排,此时a的值是:" + a);
}
}
}
public static void main(String[] args){
Semaphore windows = new Semaphore(100);//声明100个窗口,这里声明默认的是非公平锁,防止资源耗尽
for(;;){
DisorderTest disorderTest = new DisorderTest();
new Thread(new Runnable() {
@Override
public void run() {
try {
windows.acquire();//占用窗口
disorderTest.reader();
windows.release();//释放窗口,这里释放以后,第后面的线程才能继续获取
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
windows.acquire();//占用窗口
disorderTest.writer();
windows.release();//释放窗口
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
分析:
上面代码用两组线程分别访问writer()和reader(),尽可能在多线程情况下让程序产生二义性。正常情况下,无论如何if(a == 0)这段代码是不会被执行的,如果出现了,就证明在访问wirter()的时候发生了指令重排。下面对可能的情况进行图解说明
只有在情况四的场景下,发生了指令重排,下图是我执行的运行结果,确实是能发生,这个程序跑了一个下午才出现一次
参考博客
https://www.cnblogs.com/yanghongfei/p/8621508.html
https://www.cnblogs.com/chenyangyao/p/5269622.html
https://my.oschina.net/u/4124756/blog/3165255
https://www.cnblogs.com/amei0/p/8378625.html