内存屏障
引入
我们知道 volatile 能保证 JMM约束的 可见性和有序性。
关于有序性,到底该如何理解?
有序性的根本保证,就是 禁止指令重排序
重排序:
重排序是指 编译器和处理器 为了优化程序性能 而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序。
- 不存在数据依赖关系,可以重排序
- 存在数据依赖关系,禁止重排序
但是重排序后的指令,绝不能改变原有的串行语义,这点在并发编程中 重点考虑
概念
内存屏障
也称为内存栅栏,屏障指令等,是一类同步屏障指令 ,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后,才可以执行此点之后的操作。避免了代码重排序。
内存屏障其实是一种JVM指令,java内存模型的重排规则会要求Java编译器在生成JVM指令时 插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了java内存模型中的可见性和有序性(禁止重排序),但Volatile无法保证原子性。
- 内存屏障之前的所有写操作 都要写回到主内存
- 内存屏障之后的所有读操作 都能获得内存屏障之前的所有写操作的最新结果(实现可见性)
写屏障(Store Memory Barrier): 告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。 |
读屏障(Load Memory Barrier): 处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。 |
因此重排序时,不允许把内存屏障之后的指今重排序到内存屏障之前。一句话,对一个volatie变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。
内存屏障的分类
前文介绍的 happens-before 先行发生原则,类似接口规范,那么 我们如何对此规范进行落地实现?
实现它的 就是 内存屏障
内存屏障 可以分为 读屏障 (Load Barrier) 和 写屏障(Store Barrier)
- 读屏障 (Load Barrier)
在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据
- 写屏障(Store Barrier)
在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
我们对这个屏障进行源码分析 看看它到底是什么样子的
源码下载连接如下:
openJDK8u
unsafe.class
OrderAccess.hpp
屏障的真实底层处理内容 是汇编语句
对二者进一步划分 可以划分为:
- 读读屏障(LoadLoad)
- 读写屏障(LoadStore)
- 写写屏障(StoreStore)
- 写读屏障(StoreLoad)
屏障类型 | 指令类型 | 说明 |
---|---|---|
loadLoad | Load1;LoadLoad;Load2 | 保证load1的读取操作在 load2及后续读取操作之前执行 |
StoreStore | Store1;storeStore;Store2 | 在store2及其以后的写操作执行前,保证store1的写操作已经刷新到主内存 |
LoadStore | Load;LoadStore;Store2 | 在store2及以后的写操作执行前,保证load1的读操作已读取结束 |
StoreLoad | Store1;StoreLoad;Loaded | 保证store1的写操作已经刷新到主内存之后,load2及以后的读操作才能执行 |
我们整理一下 关于JMM、重排序的思路
- 重排序有可能影响程序的执行和实现,因此,我们有时候希望告诉JVM你别“自作聪明”给我重排序,我这里不需要排序,听主人的。
- 对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序。(happen-before)
- 对于处理器的重排序,Java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序。
我们通过一个Demo 来更具象的理解:
/**
* @Title:
* @Description: TODO
* @author: Alex
* @Version:
* @date 2023-02-17-0:43
*/
public class FenceDemo {
volatile static int a = 1;
public static void main(String[] args) {
new Thread(() -> {
while (true) {
//第一个操作是volatile读
System.out.println(Thread.currentThread().getName() + "\t" +
" get a value before every operations= " + a);
if (a > 1) {
//第三个操作是volatile读
System.out.println(Thread.currentThread().getName() + "\t" +
"second get a value = " + a); //load
break;
}
}
}, "t1").start();
new Thread(() -> {
synchronized (FenceDemo.class) {
//第二个操作是 volatile写 之间存在内存屏障 LoadStore 不能发生指令重排
a++; //store (存在间隙 注意非原子性)
}
}, "t2").start();
}
}
输出结果如下:
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 get a value before every Load= 1
t1 second get a value = 2
这个例子符合了我们预想的,
- 先读读看a 的值是 多少? load1
- 修改a的值 store2
- 判定a>1 再次去执行 读操作 load3
load 1 //volatile读操作
LoadStore
读写屏障 保证先读到初值,而不是被修改后的值(防止load1被重排)
store2 //volatile 写操作
StoreLoad
//写读屏障 防止后续读操作,读到未被正确修改的值
load3 //volatile读操作
形象的总结:
一锅汤,我先尝尝咸淡(load1) ,味道淡了,加点盐(store2), 再去常常咸淡(load3)
这样一系列操作,也是符合常理的,如果先加盐,再尝咸淡,那就离谱了。
下一步,我们加深案例难度:
如果出现普通读呢?
//TODO
01点34分 2023年2月17日