目录
什么是内存屏障?
小陈:老王,上一篇你引出了volatile底层是通过内存屏障来解决可见性和有序性问题的。首先我想问一下什么是内存屏障?
老王:内存屏障啊,本质上也是一种指令,只不过它具有屏障的作用而已。
小陈:额,这怎么说...
老王:首先内存屏障是一种指令,无论是在JAVA内存模型还是CPU层次,都是有具体的指令对应的,是一种特殊的指令。
小陈:嗯嗯,它是一种特殊的指令。还是不明白......
小陈:然后呢?
老王:然后这种指令具有屏障的作用,所谓屏障,也就是类似关卡,类似栅栏,具有隔离的作用。
小陈:那它是怎么实现隔离作用的,能否搞个例子讲一下。
老王:哈哈,这个举例没问题,我先跟你说说内存屏障的分类,后面再给你实例讲一下:
老王:按照内存屏障的分类,我理解有两类。
(1)一类是强制读取主内存,强制刷新主内存的内存屏障,叫做Load屏障和Store屏障
(2)另外一类是禁止指令重排序的内存屏障,有四个分别叫做LoadLoad屏障、StoreStore屏障、LoadStore屏障、StoreLoad屏障
老王:下面再给你介绍一下这两类内存屏障各自的作用:
强制读取/刷新主内存的屏障
Load屏障:执行读取数据的时候,强制每次都从主内存读取最新的值。
Store屏障:每次执行修改数据的时候,强制刷新回主内存。
先给你画图讲解一下Load屏障:
如上图所示:在工作内存的变量名、变量的值之前有一道关卡或者栅栏,导致变量 i 获取不到工作内存中的值,所以每次只好主内存重新加载咯。
然后再给讲一下Store屏障:
如上图所示,每次执行assign指令将数据变更之后,后面都会紧紧跟着一个Store屏障,让你立刻刷新到主内存。
老王:小陈,我画这两图讲强制读取和刷新主内存的屏障(Load屏障和Store屏障),你看懂了嘛?
小陈:也就是说,只要加了Load屏障,相当于加了一个栅栏,不管工作内存是否有数据,都是从主内存读取数据。只要加了Store屏障,具有强制作用,进行assign操作将变量更改了之后,立刻将变量刷新到主内存里面是吗?
老王:是的,就是这么一个道理,上面的图只是为了方便你理解画出来的,实现上并不一定完全跟图的一样,但是原理差不多。
小陈:嘿嘿,老王真棒......
禁止指令重排序的屏障
老王:好了,下面再给你讲讲另外一类的内存屏障,下面这类的内存屏障的作用是禁止指令重排序,JAVA内存模型层次关于禁止重排序有下面4种屏障:
LoadLoad屏障
序列:load1指令 LoadLoad屏障 load2指令
作用:在load1指令和load2指令之间加上 LoadLoad屏障,强制先执行load1指令再执行load2指令;load1指令和load2指令不能进行重排序(LoadLoad屏障 前面load指令禁止和屏障后面的load指令进行重排序)。
StoreStore屏障
序列:store1指令 StoreStore屏障 store2指令
作用:在store1指令和store2指令之间加上StoreStore屏障,强制先执行store1指令再执行store2指令;store1指令不能和store2指令进行重排序(StoreStore屏障 前面的store指令禁止和屏障后面的store指令进行重排序)
LoadStore屏障
序列:load1指令 LoadStore屏障 store2指令
作用:在load1指令和store2指令之前加上LoadStore屏障,强制先执行load1指令再执行store2指令;load1指令和store2执行不能重排序(LoadStore屏障 前面的load执行禁止和屏障后面的store指令进行重排序)
StoreLoad屏障
序列:store1指令 StoreLoad屏障 load2指令
作用:在store1指令和load2指令之间加上StoreLoad屏障,强制先执行store1指令再执行load2指令;
store1指令和load2指令执行不能重排序(StoreLoad屏障 前面的Store指令禁止和屏障后面的Store/Load指令进行重排)
老王:小陈,我上面说的这几种内存屏障,你能理解吗?
小陈:感觉听着有点玄乎啊,实际上是怎么禁止重排序的啊...
老王:我下面给你画个图,以StoreStore屏障和StoreLoad屏障举个例子,你就知道大概是什么意思了:
(1)有三个区域分别是区域1、区域2、区域3
(2)区域1和区域2加了 StoreStore屏障,这样区域1和区域2的Store指令就被隔离开来,不能重排了
(3)区域2和区域3加了StoreLoad屏障,这样区域2和区域3的Store指令、Load指令就被隔离开来,不能重排了
(4)就相当于搞了个栅栏,禁止各个区域之间的指令跳来跳去的,否则就会导致乱序执行
小陈:哎呀,原来是搞了个栅栏啊,说白了就是相当于搞了个围墙,不让各个指令之间跳来跳去的,这样达到禁止区域之间重排序的效果。
小陈:牛啊,老王;看起来这么深奥的道理,竟被你搞个图这么简单的说清楚了,献上我的膝盖...
老王:嘿嘿,还好还好......
小陈:内存屏障的底层是怎么保证可见性和有序性的,通过这次讨论我明白了。但是volatile是怎么使用内存屏障的我还不明白啊?
老王:别急啊,小陈。慢慢来,今天我们先讲到内存屏障这里,volatile怎么使用内存屏障的?我们下一章再说一下。
其实不只是volatile使用到了内存屏障,还有像是synchronized关键字底层也是用到内存屏障的。
今天我们先说到这里,下一篇《volatile怎么通过内存屏障保证可见性和有序性? 》我们来继续讨论
volatile通过内存屏障保证可见性
小陈:老王,你上一篇抛出一个问题volatile怎么通过内存屏障保证可见性和有序性?我现在迫不及待的想知道了。
老王:嗯嗯,我们慢慢来讲,先说说volatile怎么通过内存屏障来保证可见性?
小陈:volatile关键字实际上是怎么使用内存屏障的呢?
老王:是这样子的。
volatile修饰的变量,在每个读操作(load操作)之前都加上Load屏障,强制从主内存读取最新的数据。每次在assign赋值后面,加上Store屏障,强制将数据刷新到主内存。
老王:以volatile int = 0;线程A、B进行 i++ 的操作来画图给你讲解一下:
如上图所示:
(1)线程A读取 i 的值遇到Load屏障,需要强制从主存读取得到 i = 0; 然后传递给工作线程执行++操作
(2)cpu执行 i++ 操作得到 i = 1,执行assign指令进行赋值;然后遇到Store屏障,需要强制刷新回主内存,此时得到主内存 i = 1
(3)然后线程B执行读取 i 遇到Load屏障,强制从主内存读取,得到最新的值 i = 1,然后传给工作线程执行 ++操作,得到 i = 2,同样在赋值后遇到Store屏障立即将数据刷新回主内存
老王:通过上面的图和讲解,以及volatile读取前加的Load屏障、赋值后加的Store屏障看懂了吗?
小陈:哦哦,通过这样说我就明白了。
其实说白了就是通过一个屏障让volatile的变量每次读都读主存,每次修改后立即刷到主存里面。
好比线程A修改 i 后立即将值刷到主存里面,后面线程B用到的时候强制从主存读取,这个时候它能看到的值是线程A修改之后的值了。也就是通过这种方式来保证多线程之间的可见性吧。
老王:嘿嘿,没错;就是这个意思......
小陈:volatile通过内存屏障每次走主存的方式;这样来保障可见性,我理解了,害~,感觉也不难嘛......
老王:哈哈,这个本来就不难,只是你需要先了解一下内存屏障,以及这些屏障的作用是什么。再加上之前讲过多核CPU高速缓存、JAVA内存模型,你理解起来就很容易了。如果没有之前的知识做铺垫,你理解起来就费劲了......
小陈:哈哈,好像也是啊......
老王:好了,下面我们继续来讨论一下volatile怎么通过内存屏障来保证有序性?
volatile通过内存屏障保证有序性
老王:小陈啊,之前讲过一个有序性问题导致异常的例子,你还记得不?
小陈:记得啊,我记得当时是这样说的:
线程A的执行代码:
// 步骤1
dataSource = initDataSource();
// 步骤2
httpClient = initHttpClient();
// 步骤3
initOK = true;
线程B的执行代码:
// 步骤4
while(!initOK) {
}
// 步骤5
Object data = dataSource.getData();
// 步骤6
httpClient.request(data);
由于线程A先执行了initOK = true。导致线程B提前跳出了while循环!!! ,然后线程B调用dataSource.getData的时候发现dataSource没初始化好,竟然是个坑爹的null,导致代码报错了。
老王:哈哈,看来你很用功啊;之前的例子你都记得。
老王:现在我们就来讲讲将initOk用volatile来修饰,是可以做到线程A有序性执行的。
好了,废话不多说,我先来上代码:
// 步骤1
dataSource = initDataSource();
// 步骤2
httpClient = initHttpClient();
// 步骤3
initOK = true;
对应到指令可能是这样的:
// 步骤1 对应上面dataSource = initDataSource();
store datasource指令
// 步骤2 对应上面httpClient = initHttpClient();
store http指令
StoreStore屏障 (注意:在store initOK前面加了一个StoreStore屏障)
// 步骤3 对应上面initOK = true;
store initOk = true指令
StoreLoad 屏障 (注意:在store initOK后面加了一个StoreLoad屏障)
注意这里:store initOk指令的前面加了一道StoreStore屏障;后面加了一道StoreLoad屏障
所以通过volatile修饰initOK,加了屏障之后;store initOK = true 这一条指令是不能跳到store dataSource、store http前面去的,所以必须****先执行完前面的执行之后,才能执行store initOK = true
这样对于线程B来说,加了内存屏障之后,它看到线程A就是资源初始化完成之后,才将initOK表示设置为true的,这样它看到线程A的执行就是有序的
老王:小陈,我这么说你懂了不?
小陈:稍等,我来捋捋思路....
也就是通过加了屏障,store initOK = true 指令不能跟前面的store指令进行交换。所以它就自然得等前面的store指令执行完了之后,才执行store initOK = true的对吧? 然后在线程B那一侧看到的initOK = true的时候,发现资源以及初始化好了,自然就不会报错了。
老王:bingo,就是这个道理....
小陈:这个volatile写的时候前面加StoreStore屏障、写的后面加StoreLoad屏障来禁止重排序的我看懂了。当volatile读的时候加什么屏障来禁止重排序?
老王:这个就当作思考题,你自己再去看看咯,原理也是一样的......
小陈:好的老王,在线程安全来说volatile保证了可见性、有序性了;我看过一些资料说volatile是不能保证原子性的,那它为啥不能保证原子性啊?
老王:今天我们先讲到这里,你先消化消化。我们下一章再来讨论volatile不能保证原子性的问题......
volatile为什么不能保证原子性
小陈:老王,快来快来,上一篇结尾说volatile不能保证原子性,我现在迫不及待了...
老王:哈哈,来了,马上开搞......
老王:按照惯例,我还是先来给你画张图:
还是以 i++ 的那个例子为例,volatile int i = 0,假如两个线程A、线程B同时对 i 进行 ++ 操作如下:
上图存在一种情况就是,线程A、线程B如果几乎同时读取 i = 0 到自己的工作内存中。
线程A执行 i++ 结果后将 i = 1 赋值给工作内存;但是这个时候还没来的将最新的结果刷新回主内存的时候,线程B就读取主内存的旧值 i = 0 ,然后执行use指令将 i = 0的值传递给线程B去进行操作了。
即使这个时候线程A立即将 i = 1刷入主内存,那也晚了;线程B已经使用旧值 i = 0进行操作了,像这种情况计算结果就不对了。
老王:小陈,我上面的那个图讲解,你可以听懂嘛?
小陈:嗯嗯,看图解释就是,线程A的 i ++ 结果, 也就是 i = 1还没刷回主内存;线程B就执行 use指令将 i = 0传递给cpu了 ,导致线程B使用的就旧的值 i = 0去进行操作,得到结果是错的。
怎样才能保证原子性?
小陈:那如果要保证原子性,应该是怎么样子的?
老王:如果要保证原子性的话,落到底层实际还是需要进行加锁的,需要保证任意时刻只能有一个线程能执行成功。
比如在硬件层次或者对总线进行加锁,使得某一时刻只能有一个线程能执行i++ 操作,这样才能是不被中断的,才是原子性的。
现在现在这种情况,相当于就是两个线程同时进行了 i++操作,线程A的 i++ 操作还没结束;线程B的 i++ 操作就也同时进行着,这种情况不是原子的。
小陈:哦,是不是可以这么理解:
如果要保证原子性的话,同一时刻只能有一个线程或者CPU能够执行成功,底层是需要对硬件进行加锁的,只有某个CPU或者线程锁定了,享有独占的权限,那么它的操作才能是不被其它CPU或者线程打断的。
老王:没错,就是这个道理;你只有在硬件级别加锁了之后,享有独占的权限;你的操作才能是不被其它CPU或线程打断的。
小陈:好的,老王,这么说我就理解了。
老王:这一篇对volatile不能保障原子性的解释,你再多看看几遍,多理解一下。再对之前的文章再复习复习,包括内存屏障、java内存模型、MESI一致性协议等知识,从下一章看是我们就要进入新的学习了。
作者:终有救赎
链接:https://juejin.cn/post/7281444213693710393
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。