什么是乱序执行
CPU运行的时候,是按照指令一条一条执行的。CPU速度特别快,但是CPU从内存去取数据的话,会很慢。这时候,就可能出现后来的指令要比先到的指令先执行的情况,例如:现在给CPU两条指令 ,两个指令没有关系。第一条指令从内存读数据。需要等待很长时间,那么在等待内存的过程中,会先执行指令2。然后等数据到了以后,然后执行指令1。看起来就是指令2 比指令1先执行。这些乱序执行,是CPU自动优化的结果,可以提高效率。而且如果第二条指令依赖第一条指令,不会发生指令乱序执行。
乱序执行的测试代码:
/**
* 指令重排序验证,在这个案例代码中,可能出现场景如下:
* 1.one线程执行完毕后执行other 线程结果:a=1,x=0;b=1,y=1; xy值为:01组合
* 2.other线程执行完毕后执行one 线程结果:a=1,x=1;b=1,y=0; xy值为:10组合
* 3.other线程和one线程同时执行 线程结果:a=1,x=1;b=1,y=1; xy值为:11组合
* 按照正常逻辑,不可能出现 xy值为00的组合
*
*
* 出现这种情况,只有一种可能,出现乱序执行了。
* @author LYs
*
*/
public class T04_Disorder {
private static int x = 0,y=0;
private static int a= 0,b=0;
public static void main(String[] args) throws Exception {
int i=0;
for (; ;) {
i++;
x=0;y=0;
a=0;b=0;
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a=1;
x=b;
}
});
Thread other = new Thread(new Runnable() {
@Override
public void run() {
b=1;
y=a;
}
});
one.start();other.start();
one.join();other.join();
String result = "第"+i+"次("+x+","+y+")";
if (x==0 && y==0) {
System.err.println(result);
break;
}
}
}
}
乱序执行有可能带来的问题
有时候乱序执行会导致一些问题,单线程情况下,好像基本上没有啥影响。但是如果是多线程的话,就有可能会导致一些问题出现。举例:我们在创建对象的时候, new方法会产生4条指令。举例说明:
java代码
public class Test {
int i = 8;
}
指令如下:
1 NEW Test2 DUP
3 INVOKESPECIAL Test.<init>()V
4 ASTORE 1
其中第一行是在内存中为对象分配一块空间,这时候int i 的值是默认值0 。第二行是将指针缓存下这个不需要理解不影响分析乱序执行问题。第三行指令执行操作,将8赋值给变量i 。第四行指令将该内存的对象引用赋值给栈内的引用。当有两个线程,线程1创建test对象,同时线程2访问该对象,如果不为空,就取出i的值去进行操作。这时如果指令第三行和第四行发生重排序就会出现问题:
图片来源于马士兵线程课
如图所示,在线程1创建对象并给i默认值为0的时候,将线程的对象引用赋值给了栈中的对象变量。这时,在对该对象做非空判断时得到的结果是该对象存在。线程2就会去取线程1new出来的对象,但是线程1并没有执行指令4 init的方法。线程2 取到的值为0 但是正常操作应该取到的值为8 这就是指令重排序带来的问题。
指令重排序问题解决
指令重排序问题的解决办法是内存屏障,实现方式为:在执行的时候,加一堵墙,墙两边的指令不允许相互之间发生重排序。内存屏障的实现是在CPU级别实现的。Intel硬件提供了一系列的内存屏障,主要有:
1. lfence,是一种Load Barrier 读屏障
2. sfence, 是一种Store Barrier 写屏障
3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力
4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
java代码想要实现内存屏障,只需要在相应的变量前添加volatile关键字。这个关键字的作用为:1.保证可见性,2.防止指令重排序。jvm实现防止内存重排序使用的办法是锁定cpu总线,该办法效率较低,但是兼容性比较好。