<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          要搞懂 volatile 關鍵字,就靠這 26 張圖

          共 7119字,需瀏覽 15分鐘

           ·

          2020-11-25 08:46

          小故事

          小艾吃飯路上碰上小牛,忙問:你昨天面大廠面的咋樣了?聽說他們最喜歡問多線程相關知識。

          小牛說:對啊,第一個問題我就講了20分鐘,直接把面試官講服了。

          小艾忙問:什么問題能講這么久?是不是問你情感經歷了?

          小牛說:…問的volatile關鍵字。

          小艾說:volatile關鍵詞的作用一般有如下兩個:

          1. 可見性:當一個線程修改了由volatile關鍵字修飾的變量的值時,其它線程能夠立即得知這個修改。

          2. 有序性:禁止編譯器關于操作volatile關鍵詞修飾的變量的指令重排序。

          你說這兩個說了20分鐘?口吃?

          小牛說:你知道volatile的實現原理嗎?

          小艾說:緩存一致性協(xié)議嘛,這有啥?

          小牛說:既然硬件保證了緩存一致性協(xié)議,無論該變量是否被volatile關鍵詞修飾,它都該滿足緩存一致性協(xié)議呀。你這說的有點自相矛盾哦。

          小艾說:那volatile的實現原理是什么?

          小牛說:且聽我慢慢道來。


          緩存一致性協(xié)議

          我們知道,現代CPU都是多核處理器。由于cpu核心(Kernel)讀取內存數據較慢,于是就有了緩存的概念。我們希望針對頻繁讀寫的某個內存變量,提升本核心的訪問速率。因此我們會給每個核心設計緩存區(qū)(Cache),緩存該變量。由于緩存硬件的讀寫速度比內存快,所以通過這種方式可以提升變量訪問速度。
          緩存的結構可以如下設計:

          緩存結構圖

          其中,一個緩存區(qū)可以分為N個緩存行(Cache line),緩存行是和內存進行數據交換的最小單位。每個緩存行包含三個部分,其中valid用于標識該數據的有效性。如果有效位為false,CPU核心就從內存中讀取,并將對應舊的緩存行數據覆蓋,否則使用舊緩存數據;tag用于指示數據對應的內存地址;block則用以存儲數據,

          多核緩存和內存

          但是,如果涉及到并發(fā)任務,多個核心讀取同一個變量值,由于每個核心讀取的是自己那一部分的緩存,每個核心的緩存數據不一致將會導致一系列問題。緩存一致性的問題根源就在于,對于某個變量,好幾個核心對應的緩存區(qū)都有,到底哪個是新的數據呢?如果只有一個CPU核心對應的緩存區(qū)有該變量,那就沒事啦,該緩存肯定是新的。

          所以為了保證緩存的一致性,業(yè)界有兩種思路:

          1. 寫失效(Write Invalidate):當一個核心修改了一份數據,其它核心如果有這份數據,就把valid標識為無效;

          2. 寫更新(Write update):當一個核心修改了一份數據,其它核心如果有這份數據,就都更新為新值,并且還是標記valid有效。

          業(yè)界有多種實現緩存一致性的協(xié)議,諸如MSI、MESI、MOSI、Synapse、Firefly Dragon Protocol等,其中最為流行的是MESI協(xié)議。

          MESI協(xié)議就是根據寫失效的思路,設計的一種緩存一致性協(xié)議。為了實現這個協(xié)議,原先的緩存行修改如下:

          緩存結構圖

          原先的valid是一個比特位,代表有效/無效兩種狀態(tài)。在MESI協(xié)議中,該位改成兩位,不再只是有效和無效兩種狀態(tài),而是有四個狀態(tài),分別為:

          1. M(Modified):表示核心的數據被修改了,緩存數據屬于有效狀態(tài),但是數據只處于本核心對應的緩存,還沒有將這個新數據寫到內存中。由于此時數據在各個核心緩存區(qū)只有唯一一份,不涉及緩存一致性問題;

          2. E(Exclusive):表示數據只存在本核心對應的緩存中,別的核心緩存沒這個數據,緩存數據屬于有效狀態(tài),并且該緩存中的最新數據已經寫到內存中了。同樣由于此時數據在各個核心緩存區(qū)只有一份,也不涉及緩存一致性問題;

          3. S(Shared):表示數據存于多個核心對應的緩存中,緩存數據屬于有效狀態(tài),和內存一致。這種狀態(tài)的值涉及緩存一致性問題;

          4. I(Invalid):表示該核心對應的緩存數據無效。

          看到這里,大家想必知道為什么這個協(xié)議稱為MESI協(xié)議了吧,它的命名就是取了這四個狀態(tài)的首字母而已。

          為了保證緩存一致性,每個核心要寫新數據前,需要確保其他核心已經置同一變量數據的緩存行狀態(tài)位為Invalid后,再把新數據寫到自己的緩存行,并之后寫到內存中。

          MESI協(xié)議包含以下幾個行為:

          • 讀(Read):當某個核心需要某個變量的值,并且該核心對應的緩存沒這個變量時,就會發(fā)出讀命令,希望別的核心緩存或者內存能給該核心最新的數據;

          • 讀命令反饋(Read Response):讀命令反饋是對讀命令的回應,包含了之前讀命令請求的數據。舉例來說,Kernel0發(fā)送讀命令,請求變量a的值,Kernel1對應的緩存區(qū)包含變量a,并且該緩存的狀態(tài)是M狀態(tài),所以Kernel1會給Kernel0的讀命令發(fā)送讀命令反饋,給出該值;

          • 無效化(Invalidate):無效化指令是一條廣播指令,它告訴其他所有核心,緩存中某個變量已經無效了。如果變量是獨占的,只存在某一個核心對應的緩存區(qū)中,那就不存在緩存一致性問題了,直接在自己緩存中改了就行,也不用發(fā)送無效化指令;

          • 無效化確認(Invalidate Acknowledge):該指令是對無效化指令的回復,收到無效化指令的核心,需要將自己緩存區(qū)對應的變量狀態(tài)改為Invalid,并回復無效化確認,以此保證發(fā)送無效化確認的緩存已經無效了;

          • 讀無效(Read Invalidate):這個命令是讀命令和無效化命令的綜合體。它需要接受讀命令反饋和無效化確認;

          • 寫回(Writeback)這個命令的意思是將核心中某個緩存行對應的變量值寫回到內存中去。

          下圖給了個一個應用MESI讀寫數據的例子。在該圖中,假設CPU有兩個核心,Kernel0表示第一個核心,Kernel1表示第二個核心。這里給出了Kernel0想寫新數據到自己緩存的例子。

          MESI工作原理
          1. 首先Kernel0先完成新數據的創(chuàng)建;

          2. Kernel0向全體其他核心發(fā)送無效化指令,告訴其他核心其所對應的緩存區(qū)中的這條數據已經過期無效。本圖例中只有一個其他核心,為Kernel1;

          3. 其他核心收到廣播消息后,將自己對應緩存的數據的標志位記為無效,然后給Kernel0回確認消息;

          4. 收到所有其他Kernel的確認消息后,Kernel0才能將新數據寫回到它所對應的緩存結構中去。

          根據上圖,我們可以發(fā)現,影響MESI協(xié)議的時間瓶頸主要有兩塊:

          1. 無效化指令:Kernel0需要通知所有的核心,該變量對應的緩存在其他核心中是無效的。在通知完之前,該核心不能做任何關于這個變量的操作。

          2. 確認響應:Kernel0需要收到其他核心的確認響應。在收到確認消息之前,該核心不能做任何關于這個變量的操作,需要持續(xù)等待其他核心的響應,直到所有核心響應完成,將其對應的緩存行標志位設為Invalid,才能繼續(xù)其它操作。

          針對這兩部分,我們可以進一步優(yōu)化:

          1. 針對無效化指令的加速:在緩存的基礎上,引入Store Buffer這個結構。Store Buffer是一個特殊的硬件存儲結構。通俗的來講,核心可以先將變量寫入Store Buffer,然后再處理其他事情。如果后面的操作需要用到這個變量,就可以從Store Buffer中讀取變量的值,核心讀數據的順序變成Store Buffer → 緩存 → 內存。這樣在任何時候核心都不用卡住,做不了關于這個變量的操作了。引入Store Buffer后的結構如下所示:

          Store Buffer結構
          1. 針對確認響應的加速:在緩存的基礎上,引入Invalidate Queue這個結構。其他核心收到Kernel0的Invalidate的命令后,立即給Kernel0回Acknowledge,并把Invalidate這個操作,先記錄到Invalidate Queue里,當其他操作結束時,再從Invalidate Queue中取命令,進行Invalidate操作。所以當Kernel0收到確認響應時,其他核心對應的緩存行可能還沒完全置為Invalid狀態(tài)。引入Invalidate Queue后的結構如下所示:

          Invalidate Queue結構

          緩存一致性協(xié)議優(yōu)化存在的問題

          上一節(jié)講了兩種緩存一致性協(xié)議的加速方式。但是這兩個方式卻會對緩存一致性導致一定的偏差,下面我們來看一下兩個出錯的例子:

          例子1:關于Store Buffer帶來的錯誤,假設CPU有兩個核心,Kernel0表示第一個核心,Kernel1表示第二個核心。

          ...
          public?void?foo(){
          ????a=1;
          ????b=1;
          }
          public?void?bar(){
          ????while(b==0)?continue;
          ????assert(a==1):"a?has?a?wrong?value!";
          }
          ...

          如果Kernel0執(zhí)行foo()函數,Kernel1執(zhí)行bar()函數,按照之前我們的理解,如果b變量為1了,那a肯定為1了,assert(a==1)肯定不會報錯。但是事實卻不是這樣的。

          假設初始情況是這樣的:在執(zhí)行兩個函數前Kernel1的緩存包含變量a=0,不包含緩存變量b,Kernel0的緩存包含變量b=0,不包含緩存變量a。


          Kernel0執(zhí)行foo()函數,Kernel1執(zhí)行bar()函數時,。這樣的話計算機的指令程序可能會如下展開:


          1. Kernel0執(zhí)行a=1。由于Kernel0的緩存行不包含變量a,因此Kernel0會將變量a的值存在Store Buffer中,并且向其他Kernel進行read Invalidate操作,通知a變量緩存無效;

          2. Kernel1執(zhí)行while(b==0),由于Kernel1的緩存沒有變量b,因此它需要發(fā)送一個讀命令,去找b的值;

            2
          3. Kernel0執(zhí)行b=1,由于Kernel0的緩存中已經有了變量b,而且別的核心沒有這個變量的緩存,所以它可以直接更改緩存b的值;

            3
          4. Kernel0收到讀命令后,將最新的b的值發(fā)送給Kernel1,并且將變量b的狀態(tài)由E(獨占)改變?yōu)镾(共享);

            4
          5. Kernel1收到b的值后,將其存到自己Kernel對應的緩存區(qū)中;

            5
          6. Kernel1接著執(zhí)行while(b==0),因為此時b的新值為1,因此跳出循環(huán);

            6
          7. Kernel1執(zhí)行assert(a==1),由于Kernel1緩存中a的值為0,并且是有效的,所以斷言出錯;

            7
          8. Kernel1終于收到了第一步Kernel0發(fā)送的Invalidate了,趕緊將緩存區(qū)的a==1置為invalid,但是為時已晚。

            8

          所以我們看到,這個例子出錯的原因完全是由Store Buffer這個結構引發(fā)的。如果規(guī)定將Store Buffer中數據完全刷入到緩存,才能執(zhí)行對應變量寫操作的話,該錯誤也能避免了。

          例子2:關于Invalidate Queue帶來的錯誤,同樣假設CPU有兩個核心,Kernel0表示第一個核心,Kernel1表示第二個核心。

          ...
          public?void?foo(){
          ????a=1;
          ????b=1;
          }
          public?void?bar(){
          ????while(b==0)?continue;
          ????assert(a==1):"a?has?a?wrong?value!";
          }
          ...

          Kernel0執(zhí)行foo()函數,Kernel1執(zhí)行bar()函數,猜猜看這次斷言會出錯嗎?

          假設在初始情況是這樣的:變量a的值在Kernel0和Kernel1對應的緩存區(qū)都有,狀態(tài)為S(共享),初值為0,變量b的值是0,狀態(tài)為E(獨占),只存在于Kernel1對應的緩存區(qū),不存在Kernel0對應的緩存區(qū)。假設Kernel0執(zhí)行foo()函數,Kernel1執(zhí)行bar()函數時,程序執(zhí)行過程如下:

          0
          1. Kernel0執(zhí)行a=1,此時由于a變量被更改了,需要給Kernel1發(fā)送無效化命令,并且將a的值存儲在Kernel0的Store Buffer中;

            1
          2. Kernel1執(zhí)行while(b==0),由于Kernel1對應的緩存不包含變量b,它需要發(fā)出一個讀命令;

            2
          3. Kernel0執(zhí)行b=1,由于是獨占的,因此它直接更改自己緩存的值;

            3
          4. Kernel0收到讀命令,將最新的b的值發(fā)送給Kernel1,并且將變量b的狀態(tài)改變?yōu)镾(共享);

            4
          5. Kernel1收到Kernel0在第一步發(fā)的無效化命令,將這個命令存到Invalidate Queue中,打算之后再處理,并且給Kernel0回確認響應;

            5
          6. Kernel1收到包含b值的讀命令反饋,把該值存到自己緩存下;

            6
          7. Kernel1收到b的值之后,打破while循環(huán);

            7
          8. Kernel1執(zhí)行assert(a==1),由于此時Invalidate Queue中的無效化a=0這個緩存值還沒執(zhí)行,因此Kernel1會接著用自己緩存中的a=1這個緩存值,這就出現了問題;

            8
          9. Kernel1開始執(zhí)行Invalidate Queue中的命令,將a=0這個緩存值無效化。但這時已經太晚了。

            9

          所以我們看到,這個例子出錯的原因完全是由Invalidate Queue這個結構引發(fā)的。如果規(guī)定將Invalidate Queue中命令完全處理完,才能執(zhí)行對應變量讀操作的話,該錯誤也能避免了。


          內存屏障

          既然剛剛我們遇到了問題,那如何改正呢?這里就終于到了今天的重頭戲,內存屏障了。內存屏障簡單來講就是一行命令,規(guī)定了某個針對緩存的操作。這里我們來看一下最常見的寫屏障和讀屏障。

          1. 針對Store Buffer:核心在后續(xù)變量的新值寫入之前,把Store Buffer的所有值刷新到緩存;核心要么就等待刷新完成后寫入,要么就把后續(xù)的后續(xù)變量的新值放到Store Buffer中,直到Store Buffer的數據按順序刷入緩存。這種也稱為內存屏障中的寫屏障(Store Barrier)。

          2. 針對Invalidate Queue:執(zhí)行后需等待Invalidate Queue完全應用到緩存后,后續(xù)的讀操作才能繼續(xù)執(zhí)行,保證執(zhí)行前后的讀操作對其他CPU而言是順序執(zhí)行的。這種也稱為內存屏障中的讀屏障(Load Barrier)。


          volatile中的內存屏障

          對于JVM的內存屏障實現中,也采取了內存屏障。JVM的內存屏障有四種,這四種實際上也是上述的讀屏障和寫屏障的組合。我們來看一下這四種屏障和他們的作用:

          1. LoadLoad屏障:對于這樣的語句

            第一大段讀數據指令;?
            LoadLoad;?
            第二大段讀數據指令;

          LoadLoad指令作用:在第二大段讀數據指令被訪問前,保證第一大段讀數據指令執(zhí)行完畢

          1. StoreStore屏障:對于這樣的語句

            第一大段寫數據指令;?
            StoreStore;?
            第二大段寫數據指令;

          StoreStore指令作用:在第二大段寫數據指令被訪問前,保證第一大段寫數據指令執(zhí)行完畢

          1. LoadStore屏障:對于這樣的語句

            第一大段讀數據指令;?
            LoadStore;?
            第二大段寫數據指令;

          LoadStore指令作用:在第二大段寫數據指令被訪問前,保證第一大段讀數據指令執(zhí)行完畢。

          1. StoreLoad屏障:對于這樣的語句

            第一大段寫數據指令;?
            StoreLoad;?
            第二大段讀數據指令;

          StoreLoad指令作用:在第二大段讀數據指令被訪問前,保證第一大段寫數據指令執(zhí)行完畢。

          針對volatile變量,JVM采用的內存屏障是:

          1. 針對volatile修飾變量的寫操作:在寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障;

          2. 針對volatile修飾變量的讀操作:在每個volatile讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障;

          通過這種方式,就可以保證被volatile修飾的變量具有線程間的可見性和禁止指令重排序的功能了。

          總結

          講了這么多,我們來總結一下。

          volatile關鍵字保證了兩個性質:

          • 可見性:可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

          • 有序性:對一個volatile變量的寫操作,執(zhí)行在任意后續(xù)對這個volatile變量的讀操作之前。

          單單緩存一致性協(xié)議無法實現volatile。

          緩存一致性可以通過Store Buffer和Invalidate Queue兩種結構進行加速,但這兩種方式會造成一系列不一致性的問題。

          因此后續(xù)提出了內存屏障的概念,分為讀屏障和寫屏障,以此修正Store Buffer和Invalidate Queu產生的問題。

          通過讀屏障和寫屏障,又發(fā)展出了LoadLoad屏障,StoreStore屏障,LoadStore屏障,StoreLoad屏障JVM也是利用了這幾種屏障,實現volatile關鍵字。


          巨人的肩膀
          1. Java多線程編程核心指南

          2. https://www.jianshu.com/p/ef8de88b1343

          3. Paul E. McKenney Memory Barriers: a Hardware View for Software Hackers

          4. https://www.cnblogs.com/xiaolincoding/p/13886559.html


          好文推薦

          你不好奇 CPU 是如何執(zhí)行任務的?

          10 張圖打開 CPU 緩存一致性的大門


          哈嘍,我是小林,一股清流,就愛圖解計算機基礎。

          如果覺得文章對你有幫助,歡迎分享給你的朋友,也給小林點個「在看」,這對小林非常重要。謝謝你們,給各位小姐姐小哥哥們抱拳了,我們下次見!

          瀏覽 40
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产99精品 | 亚洲天堂一区二区 | 日韩一区二区三区中文高清电影 | 欧美日韩国产精品 | 二区三区免费视频 |