JVM的四种内存屏障

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指令,可以让CPU寄存器中的数据失效,强制当前线程从主内存里面加载数据到CPU寄存器中

    Store屏障,是x86的”sfence“指令,在其他指令后插入sfence指令,能让当前线程写入CPU寄存器中的最新数据,写入到主内存,并让其他线程可见。

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.2 多线程下的指令重排序

以上说的是单线程的情况,期望结果等于输出结果。下面看多线程的情况,如下代码:

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,所以会出错。

posted @ 2022-05-26 17:40  yifanSJ  阅读(1317)  评论(0编辑  收藏  举报