指令重排序和内存屏障
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关键字可以禁止指令重排序,而在硬件方面实现禁止指令重排序的则是内存屏障。其中包括硬件层本来就有的LoadBarriers和StoreBarriers 和JVM封装实现的四种内存屏障。
2.1 从硬件层上
内存屏障分为两种,LoadBarriers和StoreBarriers。
-
- LoadBarriers:在执行屏障后一个操作前,保证已经刷新了缓存的数据,也就是说使缓存失效,强制从内存刷新数据到缓存中。
i = a; LoadBarriers; // ..其他操作
如上伪代码中,在执行其他操作之前必须保证a的变量从主内存中读取并且刷新到缓存中。
- StoreBarriers:此屏障之前的写入缓存中的数据同步到内存中,并且保证其他线程可见。
a = 1; b = 2; c = 3; StoreBarriers; // ..其他操作
如上伪代码中,保证在其他操作之前,写入缓存中的a,b,c三个变量同步到主内存中,并且其他线程可以观察到变量的变化。
- LoadBarriers:在执行屏障后一个操作前,保证已经刷新了缓存的数据,也就是说使缓存失效,强制从内存刷新数据到缓存中。
2.2 JVM实现的内存屏障
- 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和之后的读操作重排序。
- LoadStore:对于Load1;LoadStore;Store1来说,保证Load1操作先于Store1以及后面的Store操作,即对后Store操作可见。如:
int i = a; LoadStore b = 1;
// int i = a对于b = 1及之后的store操作均可见。 - StoreLoad:同上,Store1;StoreLoad;Load1情况来说,保证Store1操作先于后续的所有Load操作,并且其Store的变量操作对其他处理器可见。由于Store操作会立即刷新到内存并对其他处理器缓存可见的特性,其具备其他三个屏障的功能,但是相对的,其花费的开销较大。
- 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关键字没有原子性的原因。
文章若有不正之处,还望指出,在此多谢!