对线面试官 - 硬件级别之再谈Volatile关键字的指令重排序

2023-09-18 17:54:07 浏览数 (2)

面试官:之前从硬件级别聊了可见性的相关问题。这次能能简单从硬件级别聊聊指令重排吗?

派大星:当然可以,说到有序性就需要提到编译后的代码的执行顺序:Java中有两种编译器,一个是静态编译器(javac)一个是动态编译器(JIT)

  • javac负责把.java文件中的源代码编译为.class文件中的字节码,这个一般是程序写好之后进行编译的。
  • JIT则是负责把.class文件中的字节码编译为JVM所在操作系统支持的机器码。一般在程序运行过程中进行编译。

为什么要提到这两个编译器呢?

原因就是由于在这个编译过程中,编译器是很有可能调整代码的执行顺序,从而提高代码的执行效率。尤其是JIT编译器对指令重排序还是挺多的。同时处理器也有可能对JIT编译好的指令进行重排序执行。还有一种情况则是:内存重排序,简单来说就是处理器在实际执行指令的过程中。在高速缓存和写缓冲器或者无效队列等等硬件层面的组件,也可能会导致指令的执行顺序是有变化的。导致其它处理器看到你的执行顺序是错乱的。

但是话又说回来,指令重排序不是胡乱的排序,它们会遵循一个关键的规则,就是数据依赖规则,如果说一个变量的结果依赖于之前的代码执行的结果,那么就不能随意进行重排序,要遵循数据的依赖。此外,之前有提到过的happens-before原则,就是有一些基本的规则时要遵守的,不会让你胡乱重排序

面试官:上面你提到了JIT指令重排序,这个你能简单说说嘛?或者说new 一个新的对象,JIT会存在指令重排序的可能吗?(JIT指令重排序的经典案例)

派大星:可以,其实 new一个新对象存在指令重排序的可能。具体情况如下:

假设我们编写这样一行代码:MyObject myObj = new MyObject();这行代码大致会执行三个步骤。

  • 步骤一:以MyObject类作为原型,给它的对象实例分配一块空间。如下objRef就是指向了分配好的内存空间的地址的引用。
代码语言:javascript复制
objRef = allocate(MyObject.class);
  • 步骤二:针对分配好内存空间的对象实例执行它的构造函数,并对这个对象实例进行初始化操作。主要就是执行我们自己写的构造函数里的一些代码,对各个实例变量赋值,初始化的逻辑。
代码语言:javascript复制
invokeConstructor(objRef);
  • 步骤三:步骤一步骤二执行完成后,一个对象实例就创建完成了。此时就是把objRef指针指向的内存地址,赋值给我们自己的引用类型的变量。此时myObj就可以作为一个类似指针的概念指向了MyObject对象实例的内存地址。属于是给我们自己创建的myObj变量进行赋值操作
代码语言:javascript复制
myObj = objRef;

以上就是new对象的一个过程,但是JIT动态编译为了加快程序的执行速度,步骤2是在初始化一个对象实例(比如里面执行一些磁盘读写,网络通信等等),JIT动态编译则可能重排为步骤一 、步骤三、步骤二。但是一旦这样进行指令重排则有可能会出现一些问题,可看如下代码:

代码语言:javascript复制
public class MyObject{
    private Resource resource;

    public MyObject(){
        this.resource = loadResource();// 从配置文件里加载数据构造Resource对象
    }

    public void execute(){
        this.resource.execute();
    }
}

// 线程1执行
MyObject myObj = new MyObject()

// 线程2执行
myObj.execute();

一旦出现线程1 刚刚执行完步骤一和步骤三,步骤二还没有执行,此时myObj已经不是null,但是MyObject对象实例内部的resource还是null。

线程2直接执行了myObj.execute()方法,此时内部会调用resource.execute()方法,但是此时resource还是空的就会报NPE。

其实DCL单例模式中就是会出现JIT指令重排,如果不加Volatile关键字则会有一些问题的发生。Volatile是保证了步骤1、2、3全部执行完毕后,才可使用myObj对象。

面试官:不错,理解的很透彻呀。

面试官:那你了解你上面提到的处理器的指令乱序执行的机制吗?

派大星:有过一些了解简单来说就是:

  • 指令乱序执行机制:指令不一定拿到了就可以立马执行,比如有的执行需要网络通信、磁盘读写、获取锁等等,有的指令不是立马就执行的,为了提升效率,在现代的很多处理器里面走的都是乱序执行的机制。乱序执行完成后,会将每个指令的执行结果放到重排序处理器里面,重排序处理器的作用是把各个指令的结果按照接收指令时的顺序放到硬件组件里面去(高速缓存或写缓冲器里面)
  • 猜测执行机制:所谓的猜测执行机制就是很有可能会执行if里的代码算出来结果后,再去判断if条件是否成立。这样也会导致代码的执行顺序和想象的不一样。

面试官:你能聊聊上面你提到过的内存指令重排序吗?(高速缓存和写缓冲器的内存重排序造成的视觉假象)

派大星:可以。首先我们要知道一个概念就是:处理器将数据写入缓冲器里,这个过程被称之为store,从高速缓存中读取是数据,这个过程是load。写缓冲器和高速缓存执行load和store的过程都是按照处理器指示的顺序执行的。处理器的重排序处理器也是按照程序顺序执行的load和store的。

但是有个问题:就是在其它处理器可能会看到的一个视觉假象,就是有可能出现看到的load和store的执行顺序并非真实的执行顺序而是重排序的。这也就是内存重排序。内存重排序一共有4种:

  1. LoadLoad重排序:一个处理器先执行一个L1读操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再L1.
  2. StoreStore重排序:一个处理器先执行一个W1写操作,再执行一个W2写操作,但是另外一个处理器看到的是先W2再W1.
  3. LoadStore重排序:一个处理器先执行一个L1读操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再L1
  4. StoreLoad重排序:一个处理器先执行了一个W1写操作,再执行一个L2读操作,但是另外一个处理器看到的是先L2再W1

以上也就是内存重排序的几种情况。

0 人点赞