什么是乱序执行

  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的值去进行操作。这时如果指令第三行和第四行发生重排序就会出现问题:

Java打乱集合_内存屏障

 

   图片来源于马士兵线程课

  如图所示,在线程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总线,该办法效率较低,但是兼容性比较好。