volatile到底解决了什么问题?
? ? ? ? 本文面向的读者是对java熟悉,并对volatile有一定的了解的java programer。(volatile简介:https://www.ibm.com/developerworks/cn/java/j-jtp06197.html? ?建议先看前几段了解下即可。不看happens-before等java理论)?市面上对该关键字的解读,包括《并发编程的艺术》中都对最底层的部分含糊不清,我相信作者是非常了解所有问题的,但是并没有找到一个确定、明了的答案。本文便是对此专门进行突破讲解。 ? ? ? 我们先抛开volatile不谈,来看看计算机的存储结构(也可以叫做存储架构,架构吹起来比较NB)。在原本的设想中,我们认为cpu是集算术计算(加减乘除)、逻辑计算(与、或、异或等)、移位功能(左移右移),而存储器就是存放指令、数据的部件。但是在实际情况下,cpu的频率越来越高(由时钟同步器决定),也就是说处理速度越来越快。如3.8GHz表示时钟的变化频率为每秒 3.8 * 10^9 = 38亿次,即每次计算只用1/3.8 = 0.26ns,这还只是单核,如果是四核,则cpu处理速度最快可达0.07ns,相比动辄ms级的访存,甚至是秒级的I/O,cpu的处理耗时就仿佛是时间暂停了一般,概率论千分之一即为小概率事件可忽略,类比过来,在本文假设的理想环境下多核cpu的处理速度悬殊高达上万至十万倍。(此处假设为最理想的情况,不考虑发热影响,以及其他电气特性影响,每一次变化就能完成一次算术或逻辑或移位计算,实际情况是无法达到的)。 ? ? ? ?目前的主存存取速度大概是ms级(数据源于去年看的书,摘自某知名网站)。cpu与主存速度的不匹配已经是天堑鸿沟,而大量的指令是含操作数的(如a + b,其中一定包含对a和b的读取指令),也就需要访存,cpu将会有大量时间耗费在等待存取周期(即读写周期)上。那cpu不能在等待期间做别的吗?答案是不可以。指令具有原子性,cpu必须完成整个指令以后才能处理下一条,至于为啥要原子性,感兴趣的同学可以自行搜索下原子性(此处以a + b为例,其中包含对a,b读取的指令,如果指令没有原子性,那么当a已经读出暂存在寄存器中,cpu继续读b,指令刚发出还未等b存入寄存器就开始求和,我们将得到 a + b = a的错误结果)。我们看到了cpu与主存间不可避免的等待矛盾(马克思说这是对抗性矛盾,是无法消除的),因此只能加快访存速度。综合集成化(器材大小,集成度越高,芯片就可以越小,性能越高)、单位价格(每Byte要多少软妹币)、访存速度、功耗等等(其实基本就看价钱和速度),我们采取了三级存储结构:cache(高速寄存器,SRAM——静态随机存储器) - 主存(内存,DRAM——动态随机存储器),主存 - 辅存(硬盘、光盘等)。其中CPU可以直接访问cache和主存,而CPU与cache(先在cache也分三级,L1-L2-L3,L1最快,容量最小,价钱最高)的速度差距可以缩小到10倍,主存与辅存差距在100倍以上。分级存储结构得目的是:让访存速度有L1 cache的超高速度,让存储空间有硬盘的大小。 ? ? ? ?上面的内容告诉了我们,为什么我们不得不分存储结构(还是因为没钱惹的祸,如果我年少有为又有钱,搞1TB cache在电脑里……)。主存-辅存这级和OS的虚拟存储以及数据库相关,我们此处仅讨论随时伴随程序运行的cache-主存。 ? ? ? ?访存分读(取)和写(存)两个类型,我们要做的事情就是让cpu的读写都尽量仅针对cache,避免与主存交互。在讨论具体的读写操作之前,我们要知道计算机中的局部性原理——即将要用到的信息很可能是正在使用的信息,因为程序存在循环(时间局部性);即将要用到的信息很可能存放在现在在用信息的邻近存储单元,因为程序通常是连续存放的(空间局部性) 基于这一原理,我们将主存和cache按同等大小分块,每次cache与主存同步就按块为最小单位进行刷新(可了解下 cache和主存的映射方式,知道cache命中判断,此处不做赘述)。CPU读取的时候只要访问cache,数据命中则认为读取成功,如果未命中,再到主存读取,并且将该数据所在块刷入cache中。而CPU写的时候也是直接写入cache(主存写入比读取更费时),然后再按策略写入主存,如全写法、写回法、写分配法、非写分配法。以下对此展开讲解,这也是volatile要解决的关键问题所在。 ? ? ? ?全写法:当写命中时(要写入的数据所在块已经加载在cache中),CPU直接将新的数据同时写入cache和主存,这样的话写入速度就等于主存的写速度了,这完全就是博尔特在陪我这种运动弱将散步,失去了cache的意义。因此全写法通常会配合一个WB(Write Buffer,写缓冲器)同时写入cache和写缓冲。 (其余cache写策略待补,请读者先自查 写回法,另外两种算法是cache写不命中时采用的,影响不大) ARM架构的cache :https://www.reader8.cn/jiaocheng/20131028/2083384.html? 这样使得cpu不用再关心主存的写入,而是由缓冲器自行写入。由于缓冲器的存在,在多核情况下全写法无与伦比的优势就出现了问题:一致性——保证cache与主存的数据一致。由于CPU是先读cache,命中失败才去读主存,因此单核场景中,CPU读到的数据一定是正确的。然而到了多核场景就不一样了。cache以及写缓冲器都是在处理器内部的,因此多核就会有多个写缓冲器,而多核共用一个主存,就会产生两个CPU同时写入同一个主存单元得情况。当CPU1写入数据以后,缓冲器刷入内存以前,CPU2读取该地址的存储单元,将会读到过时的数据。 ? ? ? ? 举例:李雷和韩梅梅约我当bulb,吃饭竟然还让我买单,一气之下我要和他们闹绝交,然后这对给我撒了十几年狗粮的神仙眷侣决定还钱。很倒霉的是他们都从X宝同时给我转账,(这个时候的X宝刚刚起步,还存在一些并发bug),我本来账户里面还有1000w,李雷韩梅梅各转给我100块饭钱,X宝为了体现优秀的并发编程技术,决定用两个线程来处理这两笔转账。倒霉催的是这两个线程又刚好分配在两个CPU同时进行(由此可见,并发问题发生的条件是多么的苛刻),在完成从他们俩账户扣款,并要给我的账户增加余额的时候:CPU李和CPU韩读了我的余额1000w到他们的cache中,主存君还在泡着茶翘着二郎腿,嘴角微笑,又办妥两件事,我真牛*,殊不知这两个CPU背后正干着坑我软妹币的勾当!CPU李及CPU韩都看到了我这个时候还剩1000w,于是把他们的钱还上,他们都以为我的余额应该是1000.01w了。然后他们再委托人(WB)告诉主存君,我现在余额是1000.01w,主存君看到两个1000.01w的余额也没思考(当然,主存没脑子,大脑是CPU),就把我的余额记成1000.01w了。然而我明明应该是1000.02w的,就这样活活被两个CPU+没脑的主存给坑了100块大洋。 ? ? ? ?上例就是volatile所要解决的并发问题。其解决方案晚上文章很多,感兴趣的读者可以自行搜索。此处提炼下,volatile通过jvm的指令,调用操作系统中控制cpu缓存策略的指令(即 program -> jvm -> os -> cpu),使得受volatile保护的存储块在修改时,立即写入主存;并且在写入期间其余CPU cache缓存的该存储块失效,需要重新从主存读取。近似于:使写不命中,并采用非写分配法(这句话看不懂的话就不管了吧。。这是在前面提炼部分上的一个类比而已)。可以看到,大量使用volatile会导致cpu的写速度回到主存级别,且其使用的方式也较为考究,因而成了一个许多人视而不见,见而不明的问题。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |