指令重排序和内存屏障

一、指令重排序

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

  • 编译器优化重排序: 编译器在不改变程序执行结果的情况下,为了提升效率,对指令进行乱序的编译。例如在代码中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语义:这是重排序都需要遵循的规则,其大致意思就是在 单线程 中,只要不改变程序的最终执行结果,那么为了提升性能可以改变指令执行的顺序。

二、内存屏障

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

  从硬件层上

内存屏障分为两种,LoadBarriers和StoreBarriers。

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

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

    • StoreBarriers:

      此屏障之前的写入缓存中的数据同步到内存中,并且保证其他线程可见。

      a = 1;
      b = 2;
      c = 3;
      StoreBarriers;
      // ..其他操作
      

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

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. S toreLoad: 同上,Store1;StoreLoad;Load1情况来说,保证Store1操作先于后续的所有Load操作,并且其Store的变量操作对其他处理器可见。由于Store操作会 立即刷新到内存并对其他处理器缓存可见的特性 ,其具备其他三个屏障的功能,但是相对的, 其花费的开销较大。
  4. S toreStore: 在Store1;StoreStore;Store2情况中,保证Store1操作先于Store2操作,即在Store1后续的Store操作之前,Store1操作保证刷新到内存并且对其他处理器可见。

volatile的禁止指令重排序

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

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

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

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

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

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

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

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章