指令重排序和内存屏障

1. 指令重排序

  指令重排序分为三种,分别为编译器优化重排序指令级并行重排序内存系统重排序。如图所示,后面两种为处理器级别(即为硬件层面)。

  • 编译器优化重排序:编译器在不改变程序执行结果的情况下,为了提升效率,对指令进行乱序的编译。例如在代码中A操作需要获取其他资源而进入等待的状态,而A操作后面的代码跟其没有依赖关系,如果编译器一直等待A操作完成再往下执行的话效率要慢的多,所以可以先编译后面的代码,这样的乱序可以提升不小的编译速度。
  • 指令级并行重排序:处理器在不影响程序执行结果的情况下,将多条指令重叠在一起执行,同样也是为了提升效率。
  • 内存系统重排序:这个跟之前两个不同的是,其为伪重排序,也就是说只是看起来像在乱序执行而已。对于现代的处理器来说,在CPU和主内存之间都具备一个高速缓存,高速缓存的作用主要为减少CPU和主内存的交互(CPU的处理速度要快的多),在CPU进行读操作时,如果缓存没有的话从主内存取,而对于写操作都是先写在缓存中,最后再一次性写入主内存,原因是减少跟主内存交互时CPU的短暂卡顿,从而提升性能,但是延时写入可能会导致一个问题——数据不一致
    // CPU1执行以下操作
    a = 1;
    int i = b;
    
    // CPU2执行下面操作
    b = 1;
    int j = a;

    其执行图如下:
        

     

       从上面图中我们可以看到,对于CPU来说,先将a = 1写入缓存在读取变量b,过后在写入a到主内存,而这个操作从表面上看就变成了先读取变量b,在写入a到主内存,也就是发生了重排序,所以才说这为伪重排序。

         而从上面我们也可以看出,由于CPU1和2写入的时机不同,最终可能导致读到的(a,b)变量有四种情况,分别是(0,0),(0,1),(1,0),(1,1)。例如,在两个缓存未写入主内存的时候就进行变量读取,这时候读到的就为(0,0),其他情况类推。所以Java在实现内存模型的时候会禁止特定类型的重排序。

 

   as-if-serial语义:这是重排序都需要遵循的规则,其大致意思就是在单线程中,只要不改变程序的最终执行结果,那么为了提升性能可以改变指令执行的顺序。

2. 内存屏障

  在编译器方面使用volatile关键字可以禁止指令重排序,而在硬件方面实现禁止指令重排序的则是内存屏障。其中包括硬件层本来就有的LoadBarriersStoreBarriers 和JVM封装实现的四种内存屏障。

2.1 从硬件层上

    内存屏障分为两种,LoadBarriersStoreBarriers

    • LoadBarriers:在执行屏障后一个操作前,保证已经刷新了缓存的数据,也就是说使缓存失效,强制从内存刷新数据到缓存中。
      i = a;
      LoadBarriers;
      // ..其他操作

      如上伪代码中,在执行其他操作之前必须保证a的变量从主内存中读取并且刷新到缓存中。

    • StoreBarriers:此屏障之前的写入缓存中的数据同步到内存中,并且保证其他线程可见。
      a = 1;
      b = 2;
      c = 3;
      StoreBarriers;
      // ..其他操作

      如上伪代码中,保证在其他操作之前,写入缓存中的a,b,c三个变量同步到主内存中,并且其他线程可以观察到变量的变化。

2.2 JVM实现的内存屏障

  1. LoadLoad:对于Load1;LoadLoad;Load2这样的情况,保证Load1先于Load2及之后的Load操作,且对其可见。例如:
    ...
    int i = a;
    LoadLoad;
    int j = b; 

    在这段代码中,在int j = b以及后面的Load操作中,都能见到int i = a的操作,也就是int i = a先于后面的读取操作。即,禁止int i = a和之后的读操作重排序。

  2. LoadStore:对于Load1;LoadStore;Store1来说,保证Load1操作先于Store1以及后面的Store操作,即对后Store操作可见。如:
    int i = a;
    LoadStore
    b = 1;

    // int i = a对于b = 1及之后的store操作均可见。
  3. StoreLoad:同上,Store1;StoreLoad;Load1情况来说,保证Store1操作先于后续的所有Load操作,并且其Store的变量操作对其他处理器可见。由于Store操作会立即刷新到内存并对其他处理器缓存可见的特性,其具备其他三个屏障的功能,但是相对的,其花费的开销较大。
  4. StoreStore:在Store1;StoreStore;Store2情况中,保证Store1操作先于Store2操作,即在Store1后续的Store操作之前,Store1操作保证刷新到内存并且对其他处理器可见。

2.3 volatile的禁止指令重排序

    我们都知道volatile关键字有两个语义:

    • 保证内存可见性
    • 禁止指令重排序

    其中JVM对其禁止指令重排序在硬件层面的实现就是通过在volatile修饰的变量前后插入内存屏障。volatile变量的内存屏障规则如下:

  在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
  在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

    而在编译器方面则是因为对于volatile变量内存中的六种操作会有特殊的规则,可以看看我的另一篇文章——浅谈内存模型,里面介绍了volatile两种语义的原理,同时也说明了volatile关键字没有原子性的原因。

 

 

 文章若有不正之处,还望指出,在此多谢!

posted @ 2019-10-15 21:43  张小云的博客  阅读(2433)  评论(0编辑  收藏  举报