1、为什么要有内存屏障
为了解决cpu,高速缓存,主内存带来的的指令之间的可见性和重序性问题
。
我们都知道计算机运算任务需要CPU和内存相互配合共同完成,其中CPU负责逻辑计算,内存负责数据存储。CPU要与内存进行交互,如读取运算数据、存储运算结果等。由于内存和CPU的计算速度有几个数量级的差距,为了提高CPU的利用率,现代处理器结构都加入了一层读写速度尽可能接近CPU运算速度的高速缓存来作为内存与CPU之间的缓冲:将运算需要使用的数据复制到缓存中,让CPU运算可以快速进行,计算结束后再将计算结果从缓存同步到主内存中,这样处理器就无须等待缓慢的内存读写了。就像下面这样:
每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取
,但是这样的弊端也很明显:不能实时的和内存发生信息交换,会使得不同CPU执行的不同线程对同一个变量的缓存值不同
。用volatile关键字修饰变量可以解决上述问题,那么volatile是如何做到这一点的呢?那就是内存屏障,内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样,java通过屏蔽这些差异,统一由jvm来生成内存屏障的指令
volatile的有序性和可见性
volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;由于内存屏障的作用,避免了volatile变量和其它指令重排序、实现了线程之间通信,使得volatile表现出了锁的特性。
重排序:代码的执行顺序不按照书写的顺序,为了提升运行效率,在不影响结果的前提下,打乱代码运行
int a=1;
int b=2;
int c=a+b;
int c=5;
这里的int c=5这个赋值操作可能发生在int a=1这个操作之前
2、硬件上面的内存屏障
- Load屏障,是x86上的”ifence“指令,在其他指令前插入ifence指令,
可以让高速缓存中的数据失效,强制当前线程从主内存里面加载数据
- Store屏障,是x86的”sfence“指令,在其他指令后插入sfence指令,
能让当前线程写入高速缓存中的最新数据,写入主内存,让其他线程可见
。
3、Java里面的四种内存屏障
-
LoadLoad屏障:举例语句是
Load1; LoadLoad; Load2(这句里面的LoadLoad里面的第一个Load对应Load1加载代码,然后LoadLoad里面的第二个Load对应Load2加载代码)
,此时的意思就是,在Load2及后续读取操作从内存读取数据到CPU前,保证Load1从主内存里要读取的数据读取完毕。 -
StoreStore屏障:举例语句是
Store1; StoreStore; Store2(这句里面的StoreStore里面的第一个Store对应Store1存储代码,然后StoreStore里面的第二个Store对应Store2存储代码)
。此时的意思就是在Store2及后续写入操作执行前,保证Store1的写入操作已经把数据写入到主内存里面,确认Store1的写入操作对其它处理器可见。 -
LoadStore屏障:举例语句是
Load1; LoadStore; Store2(这句里面的LoadStore里面的Load对应Load1加载代码,然后LoadStore里面的Store对应Store2存储代码)
,此时的意思就是在Store2及后续代码写入操作执行前,保证Load1从主内存里要读取的数据读取完毕。 -
StoreLoad屏障:举例语句是
Store1; StoreLoad; Load2(这句里面的StoreLoad里面的Store对应Store1存储代码,然后StoreLoad里面的Load对应Load2加载代码)
,在Load2及后续读取操作从内存读取数据到CPU前,保证Store1的写入操作已经把数据写入到主内存里,确认Store1的写入操作对其它处理器可见。
4、使用内存屏障保存Volatile的有序性
volatile关键字是通过内存屏障,禁止被它修饰的变量发生指令重排操作
4.1 单线程下的指令重排序
处理器为了提高程序运行效率,可能会对输入代码进行优化,使得程序中各个语句的执行顺序同代码中的顺序不一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。比如:
int i=10;//语句1
int j=10;//语句2
在执行时,有可能对代码进行重排序,比如先执行语句2,再执行语句1。但是如果代码,编程下边这样:
int i=0;
int j=0;
j++;//语句3
i=j+1;//语句4
这时,语句3和语句4并不会进行重排序。因为语句3和4之间有依赖关系,重排序后会影响结果。
4.1 多线程下的指令重排序
以上说的是单线程的情况,期望结果等于输出结果。下面看多线程的情况,如下代码:
boolean flag=false;
private Context context;
//线程1
context=loadContext();//语句1
flag=true;//语句2
//线程2
if(flag){
dowork(context);
}
如果线程1执行的时候,语句1和语句2进行了重排序,先执行语句2,在还没有执行语句1时,这时线程2 将要执行if,那么就会进入到if语句块中,而context还是null,所以会出错。
参考:https://www.zhihu.com/question/409020190/answer/1360584418
参考:https://www.itqiankun.com/article/1564577564