指令重排序、内存屏障很难?看完这篇你就懂了!

2,405 阅读4分钟

听说微信搜《Java鱼仔》真的可以变强哦!

面试官在问到多线程编程的时候,指令重排序、内存屏障经常会被提起。如果你对这两者有一定的理解,那这就是你的加分项。

(一)什么是指令重排序

为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入的代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,并确保这一结果和顺序执行结果是一致的,但是这个过程并不保证各个语句计算的先后顺序和输入代码中的顺序一致。这就是指令重排序。

简单来说,就是指你在程序中写的代码,在执行时并不一定按照写的顺序。

在Java中,JVM能够根据处理器特性(CPU多级缓存系统、多核处理器等)适当对机器指令进行重排序,最大限度发挥机器性能。

Java中的指令重排序有两次,第一次发生在将字节码编译成机器码的阶段,第二次发生在CPU执行的时候,也会适当对指令进行重排。

(二)复现指令重排序

光靠说不容易看出现象,下面来看一段代码,这段代码网上出现好多次了,但确实很能复现出指令重排序。我把解释放在代码后面。

public class VolatileReOrderSample {
    //定义四个静态变量
    private static int x=0,y=0;
    private static int a=0,b=0;

    public static void main(String[] args) throws InterruptedException {
        int i=0;
        while (true){
            i++;
            x=0;y=0;a=0;b=0;
            //开两个线程,第一个线程执行a=1;x=b;第二个线程执行b=1;y=a
            Thread thread1=new Thread(new Runnable() {
                @Override
                public void run() {
                    //线程1会比线程2先执行,因此用nanoTime让线程1等待线程2 0.01毫秒
                    shortWait(10000);
                    a=1;
                    x=b;
                }
            });
            Thread thread2=new Thread(new Runnable() {
                @Override
                public void run() {
                    b=1;
                    y=a;
                }
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            //等两个线程都执行完毕后拼接结果
            String result="第"+i+"次执行x="+x+"y="+y;
            //如果x=0且y=0,则跳出循环
            if (x==0&&y==0){
                System.out.println(result);
                break;
            }else{
                System.out.println(result);
            }
        }
    }
    //等待interval纳秒
    private static void shortWait(long interval) {
        long start=System.nanoTime();
        long end;
        do {
            end=System.nanoTime();
        }while (start+interval>=end);
    }
}

这段代码虽然看着长,其实很简单,定义四个静态变量x,y,a,b,每次循环时让他们都等于0,接着用两个线程,第一个线程执行a=1;x=b;第二个线程执行b=1;y=a。

这段程序有几个结果呢?从逻辑上来讲,应该有3个结果:

当第一个线程执行到a=1的时候,第二个线程执行到了b=1,最后x=1,y=1

当第一个线程执行完,第二个线程才刚开始,最后x=0,y=1

当第二个线程执行完,第一个线程才开始,最后x=1,y=0

理论上无论怎么样都不可能x=0,y=0;

但是当程序执行到几万次之后,竟然出现了00的结果:

在这里插入图片描述

这就是因为指令被重排序了,x=b先于a=1执行,y=a先于b=1执行。

在这里插入图片描述

(三)通过什么方式禁止指令重排序?

Volatile通过内存屏障可以禁止指令重排序,内存屏障是一个CPU的指令,它可以保证特定操作的执行顺序。

内存屏障分为四种:

StoreStore屏障、StoreLoad屏障、LoadLoad屏障、LoadStore屏障。

JMM针对编译器制定了Volatile重排序的规则:

在这里插入图片描述

光看这些理论可能不容易懂,下面我就用通俗的话语来解释一下:

首先是对四种内存屏障的理解,Store相当于是写屏障,Load相当于是读屏障。

比如有两行代码,a=1;x=b;并且我把 a 和 b 修饰为 volatile。

执行 a=1 时,它相当于执行了一次 volatile 写操作;

执行 x=b 时,它相当于先执行 volatile 读取 b,再执行普通写 x 等于 b;

因此在这两行命令之间,就会插入一个 StoreLoad 屏障(前面是写后面也是写),这就是内存屏障。

第一个操作是 volatile 写操作,第二个操作是 volatile 读操作,那么表格中对应的值就是 NO,禁止重排序。这就是 Volatile 进行指令重排序的原理。

现在,只需要把上面代码的 a 和 b 用 volatile 修饰,就不会发生指令重排序了。