:::tips Hotspot使用lock指令实现了内存屏障的功能,实际上并没有使用内存屏障 :::

内存屏障

Java编译器在生成指令序列的适当位置会插入内存屏障指令禁止特定类型的处理器重排序,从而让程序按我们预想的流程去执行。
1、保证特定操作的执行顺序
2、影响某些数据(或则是某条指令的执行结果)的内存可见性。将内存屏障之前的所执行命令的结果发布到其他线程中其他线程能立刻看到修改结果。
编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
Memory Barrier所做的另外一件事是强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入 cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。

四种基本内存屏障

JMM把内存屏障指令分为4类:

屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 确保Load1的数据的装载先于Load2及所有后续装载指令的装载
StoreStoreBarriers Store1;StoreStore;Store2 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
LoadStore Barriers Load1;LoadStore;Store2 确保Load1的数据的装载先于Store2及所有后续存储指令的存储
StoreLoad Barriers
(volatile使用)
Store1;StoreLoad;Load2 确保Store1的数据对其他处理器可见(刷新到内存)先于Load2及所有后续的装载指令的装载

StoreLoad Barriers 是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。

基于保守策略的 JMM 内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个 StoreStore 屏障。
  • 在每个volatile写操作的后面插入一个 StoreLoad 屏障。
  • 在每个volatile读操作的后面插入一个 LoadLoad 屏障。
  • 在每个volatile读操作的后面插入一个 LoadStore 屏障。

    内存屏障分类

    按照可见性保障来划分

    加载屏障Load Barrier

    StoreLoad屏障可充当加载屏障,作用是使用load 原子操作,即清空无效化队列,使处理器在读取共享变量时,先从主内存或其他处理器的高速缓存中读取相应变量,更新到自己的缓存中。刷新处理器缓存,同步其他处理器对该volatile变量的修改结果,保证当前读取到的变量是最新的。

    存储屏障Store Barrier

    StoreLoad屏障可充当存储屏障,作用是使用 store 原子操作,冲刷处理器缓存,即将写缓冲器内容写入高速缓存中,使处理器对共享变量的更新写入高速缓存或者主内存中刷新处理器缓存,结果是可以确保该存储屏障之前一切的操作所生成的结果对于其他处理器来说都可见。

这两个屏障一起保证了数据在多处理器之间是可见的。


按照有序性保障来划分

获取屏障Acquire Barrier

相当于LoadLoad屏障与 LoadStore屏障的组合。在读操作后插入,禁止该读操作与其后的任何读写操作发生重排序;防止上面的volatile读取操作与下面的所有操作语句的指令重排序。

释放屏障Release Barrier

相当于LoadStore屏障与StoreStore屏障的组合。在一个写操作之前插入,禁止该写操作与其前面的任何读写操作发生重排序。防止下面的volatile与上面的所有操作的指令重排序。

这两个屏障一起保证了临界区中的任何读写操作不可能被重排序到临界区之外。


CPU 架构提供的内存屏障

现在的 CPU 架构都提供了内存屏障功能,
在 x86 的 CPU 中,实现了相应的内存屏障,写屏障(Store Barrier)、读屏障(Load Barrier)和全屏障(Full Barrier)

写屏障 Store Barrier

相当于 StoreStore Barrier,强制所有在 StoreStore 内存屏障之前的所有执行,都要在该内存屏障之前执行,并发送缓存失效的信号。
所有在 StoreStore Barrier 指令之后的 Store 指令,都必须在 StoreStore Barrier 屏障之前的指令执行完后再被执行。限制了写屏障前后指令进行重排序,使得所有 Store Barrier 之前发生的内存更新都是可见的。

image.png
Store Barrier 保证 Store A 在 Store B 之前执行,并且 Store A 改变的值要同步到主内存,Store B 能够从主内存获取到最新的值。

读屏障 Load Barrier

相当于 LoadLoad Barrier,强制所有在 Load Barrier 读屏障之后的 Load 指令,都在 Load Barrier 屏障之后执行。也就是限制对 Load barrier 读屏障前后的 Load 指令进行重排序, 配合 Store Barrier,使得所有 Store Barrier 之前发生的内存更新,对 Load Barrier 之后的 Load 操作是可见的。
image.png

全屏障 Full Barrier

相当于 StoreLoad,是一个全能型的屏障,因为它同时具备前面两种屏障的效果。限制了所有在 StoreLoad Barrier 之前的 Store/Load 指令,都在该屏障之前被执行,所有在该屏障之后的的 Store/Load 指令,都在该屏障之后被执行。禁止对 StoreLoad 屏障前后的指令进行重排序。
image.png

对于读取操作来说,volatile可以确保该操作与其后续的所有读写操作都不会进行指令重排序。
对于修改操作来说,volatile可以确保该操作与其上面的所有读写操作都不会进行指令重排序。

synchronized的内存屏障

synchronized编译成字节码后,是通过monitorenter(lock原子操作抽象而来)和 monitorexit(unlock原子操作抽象而来)两个指令实现的,具体过程如下:
image.png
可以发现,synchronized底层通过获取屏障和释放屏障的配对使用保证有序性,加载屏障和存储屏障的配对使用保正可见性。最后又通过的排他性保障了原子性与线程安全。

volatile的内存屏障与源码

保证有序性,可见性

与 synchronized 类似,volatile 也是通过内存屏障来保证有序性与可见性,过程如下:

读操作

image.png

写操作

image.png
经过对比synchronized,可以发现 volatile 少了两个指令 monitorenter 与 monitorexit 用来保证原子性与线程安全。

Volatile源码

bytecodeInterpreter.cpp

storeLoad()

image.png
image.png

HotSpot的内存屏障

image.png
image.png
lock指令对总线上锁,只有当前CPU能够对当前变量进行写主存的操作。