您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
volatile保证内存可见性探究
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
volatile保证内存可见性探究
自猿其说Tech
2021-09-13
IP归属:未知
20800浏览
计算机编程
### 1 引言 volatile关键字想必大家并不陌生,我们在进行多线程编程开发时经常遇到.使用volatile的一个关键目的是为了保证内存中共享变量的可见性。下文会从cpu指令和JMM内存模型角度探究,volatile是如何保证内存可见性的。 ### 2 CPU存储器层次结构简介 我们大家都知道,cpu的运行速度是非常之快的,假如,cpu直接从硬盘读取和操作数据,会怎么样呢?那大家肯定会崩溃。因为,硬盘的运行速度是非常慢的相对于cpu来说。为了使cpu大量的时间用来计算而不是等待读取,出现了内存。但是随着cpu速度的不断提升,内存逐渐的也跟不上cpu的运行速度了为了解决这种差异,充分利用cpu的使用效率,cpu缓存出现了,他是介于cpu处理器和内存之间的临时数据交换缓冲区。下面贴一张大家非常眼熟的又非常经典的cpu存储器层次结构图。想必看过《深入理解计算机系统》一书的同学一定见过这张图。 ![](//img1.jcloudcs.com/developer.jdcloud.com/b3a5ceae-3aa3-43b9-b651-632d65113b6420210913140609.png) 我们大致可以将此图抽象为 ![](//img1.jcloudcs.com/developer.jdcloud.com/3e94014e-f1ef-4a44-bda9-6a39370347f220210913140622.png) 从图中我们可以看出,cpu和内部的高速缓存直接交互,而高速缓存则将主内存的数据加载到高速缓存中,来供cpu进行操作消费。 ### 3 JMM(Java Memory Model)java内存模型简介 Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),java线程内存模型和cpu缓存模型类似,是基于cpu缓存模型来建立的,java线程内存模型是标准化的,用于屏蔽掉各种硬件和操作系统的内存访问差异。 下图为java内存与处理器存储模型之间的关系图。 ![](//img1.jcloudcs.com/developer.jdcloud.com/de2cc5f6-9617-40d5-b1ff-23584c529a3120210913140645.png) java内存模型规定了所有的变量都存储在主内存中,每条线程有自己的工作内存。线程的工作内存中保存了被该线程使用的变量的考本副本。线程不能够直接对主内存中的变量进行读写,也就是说,线程对变量所有的操作都必须在工作内存中进行,并且,不同线程之间也无法访问彼此之间的比变量,线程间变量的传递需要通过主内存来完成,线程,主内存,工作内存的关系如下图。 ![](//img1.jcloudcs.com/developer.jdcloud.com/540f811b-cdbb-4ea7-9173-b572c8c3a82b20210913140704.png) ### 4 验证模型 为了验证上述所讲的模型,我们拿一段代码来做实验。如下图 ![](//img1.jcloudcs.com/developer.jdcloud.com/925a2f97-7232-429a-84e7-f613fe43758b20210913140719.png) 首先我们开启两个线程,定义一个共享变量flag 将其设置为 true。线程1启动循环,线程2将变量flag修改为false。运行结果证实了我们上述的JMM模型猜想,每个线程操作共享变量的时候回将共享变量的副本拷贝到自己的工作内存中,所以就算B线程已经将flag标志修改为false,1线程依旧读取自身工作内存中的flag=true。从而一直在死循环。 有没有什么方法能能够解决现在遇到的这种问题呢?熟悉并发编程的小伙伴肯定脱口而出,使用volatile修饰共享变量就可以了。我们照做来尝试一下。结果如下图。 ![](//img1.jcloudcs.com/developer.jdcloud.com/59e9d04f-18e5-4b8d-b068-ebe7db65aff120210913140734.png) 如我们所料,线程1如期结束了。为什么会出现这种情况呢?使用volatile和没有使用volatile的共享变量有什么区别呢?我们将hsdis-amd64.dll(https://github.com/atzhangsan/file_loaded) 文件放入对应jdk或者jre的bin目录下,增加jvm启动参数 -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileExample.changeFlag 对这段代码进行反编译看一下有什么不同。 **1)不使用volatile修饰** ![](//img1.jcloudcs.com/developer.jdcloud.com/97bdebfc-b900-4bfb-b06b-344eab89fb0620210913140831.png) **2)使用volatile修饰** ![](//img1.jcloudcs.com/developer.jdcloud.com/4e055a90-6675-4b15-bed5-f875fac3115720210913140952.png) 我们可以看出volatile修饰的变量反编译完成之后执行时有lock前缀修饰,下面我们来着重的研究一下这个lock前缀,lock指令就是volatile保证内存可见性的关键。通过查询资料,我们发现lock前缀修饰的指令带多核cpu中会有引发几个操作 **1. lock前缀指令会引起处理器缓存写会到主内存** **2. 指令写回主内存之后其他cpu中的缓存会相应的失效。** 这就很能说明问题了,线程2将变量改为false由于变量为volatile修饰所以直接刷新回主内存,并且,最重要的是,1线程中flag变量的副本失效,只能重新从主内存中加载,这时候,flag变量已经变为了false所以线程1可以顺利结束死循环结束运行。 那么这里有同学可能会问,为什么1线程可以感知到2线程修改了flag变量呢?是线程2向线程1发送了一条信息吗?当然不是,我们前面讲过,不同线程之间是不能直接通信的。那么这是为什么呢?原因就在于缓存一致性。下面我们就来进一步了解探究,缓存一致性 ### 5 MESI缓存一致性协议 MESI缓存一致性协议是cpu缓存一致性协议的一种。MESI协议将cache line的状态分成modify、exclusive、shared、invalid,分别是修改、独占、共享和失效。 - **modify:**当前CPU cache拥有最新数据(即最新的cache line),其他CPU拥有失效数据(cache line的状态是invalid),虽然当前CPU中的数据和主存是不一致的,但是以当前CPU的数据为准; - **exclusive:**只有当前CPU中有数据,其他CPU中没有改数据,当前CPU的数据和主存中的数据是一致的; - **shared:**当前CPU和其他CPU中都有共同数据,并且和主存中的数据一致; - **invalid:**当前CPU中的数据失效,数据应该从主存中获取,其他CPU中可能有数据也可能无数据,当前CPU中的数据和主存被认为是不一致的; MESI缓存一致性协议中,每个cache的控制器不仅知道自己的操作(local read和local write),通过监听总线也知道其他CPU中cache的操作(remote read和remote write)。对于自己本地缓存有的数据,CPU仅需要发起local操作,否则发起remote操作,从主存中读取数据,cache控制器通过总线监听,仅能够知道其他CPU发起的remote操作,但是如果local操作会导致数据不一致性,cache控制器会通知其他CPU的cache控制器修改状态。 - local read(LR):读本地cache中的数据; - local write(LW):将数据写到本地cache; - remote read(RR):读取内存中的数据; - remote write(RW):将数据写通到主存; 说了这么多理论性的东西,话题回到我们第三小节的验证上来。 首先,线程1读取贡献变量flag=true,此时所有cpu都没有这个数据,这个线程将数据从主内存中加载到cache中,发生一次RR操作,此时当前cpu cache line状态为exclusive即只有当前CPU中有数据,其他CPU中没有改数据,当前CPU的数据和主存中的数据是一致的。 再往下运行,线程2开启,此时线程2本地是没有flag共享变量的,所以从主内存读取,所以此时发生RR操作,线程1cpu cache通过总线监听,得知发生了RR操作,此时,将当前1线程cpu cache line 状态修改为shared即当前CPU和其他CPU中都有共同数据,并且和主存中的数据一致。 接下来,线程2将flag贡献变量修改为false,此时发生了一次LW操作此时cpu cache line状态修改为modify,即当前CPU cache拥有最新数据(即最新的cache line),其他CPU拥有失效数据(cache line的状态是invalid),虽然当前CPU中的数据和主存是不一致的,但是以当前CPU的数据为准; 同时在cpu2将数据写会主内存也就是发生RW操作的时候,cpu1监听到之后将自己的cpu cache line 状态修改为 invalid,此时当前CPU中的数据失效,数据应该从主存中获取,其他CPU中可能有数据也可能无数据,当前CPU中的数据和主存被认为是不一致的,此时由于cpu1 cache line 状态为invalid 所以会从主内存中从新加载flag变量,此时新的flag变量为false所以线程1结束死循环,程序执行结束。 ### 6 总结 本文浅析了volatile是如何保证内存可见性的,同时也大致的抽象介绍了JMMjava内存模型,当然volatile的作用并不是只有保证内存可见性,当然还有禁止指定重排序等等。这些会在我后面的文章中带大家一起探究学习。我所讲的这些内容只是简单的个人理解,只是并发编程的冰山一角,如果有错误或遗漏的地方欢迎各位同学一起探讨。 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:网规技术部 王子源
原创文章,需联系作者,授权转载
上一篇:一起聊聊工作中的功能安全测试
下一篇:WMS解决依赖冲突的实践
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号