一文吃透Volatile,征服面試官

TopJavaer發表於2019-10-08

根據圖所展示的知識點,有目的性的閱讀以下內容!!!

一文吃透Volatile,征服面試官

前情省略一千字....


此時,小黃心裡十分緊張的坐在面試官面前,看著面試官來回翻動自己的簡歷,準備接受狂風暴雨的洗禮。


此時,面試官抬起頭,目光如炬,看著小黃,笑了笑。


面試官:平時專案中有沒有用到volatile關鍵字?


小黃:用到了,為了在多執行緒處理器環境下能保證共享變數的可見性。


面試官:不錯,那你覺得什麼是可見性?


小黃:在多執行緒情況下,讀和寫發生在不同的執行緒中,而讀執行緒未能及時的讀到寫執行緒寫入的最新的值。


面試官:對的,那麼你覺得volatile關鍵字是如何保證執行緒的可見性呢?


小黃:我覺得,首先我們需要從硬體層面瞭解可見性的本質。一臺機算機最核心的元件是CPU,記憶體,以及I/O裝置。但是這三者在處理速度上有很大的差異,但是最終整體的計算效率還是取決於最慢的那個裝置,為了平衡三者的速度差異,最大化的利用CPU提升效能,無論是硬體,作業系統還是編譯器都做了很多的優化。


  1. CPU增加了告訴快取

  2. 作業系統增加了程式,執行緒,通過時間片切換最大化的提升CPU效能

  3. 編譯器的指令優化,更合理的去利用好CPU的快取記憶體



面試官:什麼是CPU快取記憶體?


小黃:由於計算機的儲存裝置與處理器的運算速度差距非常大,所以現代計算機系統都會增加讀寫速度儘可能接近處理器運算速度的快取記憶體來作為記憶體和處理器之間的緩衝:將運算需要使用的資料複製到快取中,讓運算能快速進行,當運算結束後再從快取中同步到記憶體之中。


面試官:很棒,但是使用CPU告訴快取會不會帶來一些問題呢?


小黃:是的,它雖然很好的解決了處理器與記憶體之間的速度矛盾,但是也引入了一個新的問題,快取一致性。


一文吃透Volatile,征服面試官

有了快取記憶體後,每個CPU處理過程變成這樣:先將計算機需要用到的資料快取在CPU快取記憶體中,在CPU進行計算時,直接從快取記憶體中讀取資料並且計算完成之後寫到快取中,在整個運算完成後,再把快取中的資料同步到記憶體。


由於在多CPU中,每個執行緒可能會執行在不同的CPU中,並且每個執行緒都有自己的快取記憶體,同一份資料可能會被快取到多個CPU中,如果在不同CPU中執行的不同執行緒看到同一份記憶體的快取值不一樣,就會存在快取不一致的問題。


面試官:有沒有什麼解決方案?


小黃:1.匯流排鎖。2.快取鎖。


面試官:解釋一下什麼是匯流排鎖?


小黃:要不我畫個圖吧,(於是有了下面這張圖),但是匯流排鎖開銷較大,所以需要優化,最好的方法就是控制鎖的粒度,我們只需要保證,對於被多個CPU快取的同一份資料是一致的就行,所以引入了快取鎖,他的核心機制就是快取一致性協議。


一文吃透Volatile,征服面試官



面試官:什麼是快取一致性協議?


小黃:為了達成資料訪問的一致性,需要各個處理器在訪問記憶體時,遵循一些協議,在讀寫時根據協議來操作,常見的協議有,MSI,MESI,MOSI等等,最常見的就是MESI協議;


MESI表示快取行的四種狀態,分別是:

    M(modify)表示共享資料只快取當前CPU快取中,並且是被修改狀態,也就是快取的資料和主記憶體中的資料不一致。

    E(Exclusive)表示執行緒的獨佔狀態,資料只快取在當前的CPU快取中,並且沒有被修改

    S(Shared)表示資料可能被多個CPU 快取,並且各個快取中的資料和主記憶體資料一致

    I(Invalid) 表示快取已經失效

在MESI協議中,每個快取的快取控制器不僅知道自己的讀寫操作,而且監聽(snoop)其他的Cache的讀寫操作


對於MESI協議,從CPU讀寫角度來說會遵循以下原則:

CPU讀請求:快取處於M,E,S狀態都可以被讀取,I狀態CPU還能從主存中讀取資料

CPU寫請求:快取處於M,E狀態下才可以被寫,對於S狀態的寫,需要將其他CPU中快取置於無效才可寫


使用匯流排鎖和快取鎖後,CPU對於記憶體的操作大概可以抽象成下面這樣的結構,從而達成快取一致性效果



一文吃透Volatile,征服面試官面試官:既然說基於快取一致性協議或者匯流排鎖就能達到一致性的要求,那麼為什麼還需要voliate關鍵字呢?


小黃:MESI優化帶來了可見性的問題:MESI 協議雖然可以實現快取的一致性,但是也會存在一些問題。就是各個 CPU 快取行的狀態是通過訊息傳遞來進行的。如果 CPU0 要對一個在快取中共享的變數進行寫入,首先需要傳送一個失效的訊息給到其他快取了該資料的 CPU。並且要等到他們的確認回執。CPU0 在這段時間內都會處於阻塞狀態。


一文吃透Volatile,征服面試官

所以為了避免阻塞帶來的資源浪費。在 cpu 中引入了 Store Bufferes。


CPU0 只需要在寫入共享資料時,直接把資料寫入到 store bufferes 中同時傳送 invalidate 訊息,然後繼續去處理其他指令。當收到其他所有 CPU 傳送了 invalidate acknowledge 訊息時,再將 store bufferes 中的資料資料儲存至 cache line中。最後再從快取行同步到主記憶體。


一文吃透Volatile,征服面試官

但是這種優化存在兩個問題

    1. 資料什麼時候提交是不確定的,因為需要等待其他 cpu給回覆才會進行資料同步。這裡其實是一個非同步操作

    2. 引入了 storebufferes 後,處理器會先嚐試從 storebuffer中讀取值,如果 storebuffer 中有資料,則直接從storebuffer 中讀取,否則就再從快取行中讀取


看下下面這個例子:

    int value=0;複製程式碼void exeToCPU0{複製程式碼  value=10;複製程式碼  isFinish=true;複製程式碼}複製程式碼void exeToCPU1{複製程式碼  if(isFinish){複製程式碼    assert value==10;  複製程式碼  }複製程式碼}複製程式碼

    exeToCPU0和exeToCPU1分別在兩個獨立的CPU上執行。


    假如 CPU0 的快取行中快取了 isFinish 這個共享變數,並且狀態為(E)、而 Value 可能是(S)狀態。


    那麼這個時候,CPU0 在執行的時候,會先把 value=10 的指令寫入到storebuffer中。並且通知給其他快取了該value變數的 CPU。在等待其他 CPU 通知結果的時候,CPU0 會繼續執行 isFinish=true 這個指令。而因為當前 CPU0 快取了 isFinish 並且是 Exclusive 狀態,所以可以直接修改 isFinish=true。這個時候 CPU1 發起 read操作去讀取 isFinish 的值可能為 true,但是 value 的值不等於 10。


    這種情況我們可以認為是 CPU 的亂序執行,也可以認為是一種重排序,而這種重排序會帶來可見性的問題。


    面試官:如何解決重排序帶來的可見性問題?


    小黃:CPU記憶體屏障用來解決這個問題,記憶體屏障就是將 store bufferes 中的指令寫入到記憶體,從而使得其他訪問同一共享記憶體的執行緒的可見性。


    X86 的 memory barrier 指令包括 lfence(讀屏障) sfence(寫屏障) mfence(全屏障)。


    Store Memory Barrier(寫屏障) 告訴處理器在寫屏障之前的所有已經儲存在儲存快取(store bufferes)中的資料同步到主記憶體,簡單來說就是使得寫屏障之前的指令的結果對屏障之後的讀或者寫是可見的。


    Load Memory Barrier(讀屏障) 處理器在讀屏障之後的讀操作,都在讀屏障之後執行。配合寫屏障,使得寫屏障之前的記憶體更新對於讀屏障之後的讀操作是可見的。


    Full Memory Barrier(全屏障) 確保屏障前的記憶體讀寫操作的結果提交到記憶體之後,再執行屏障後的讀寫操作有了記憶體屏障以後,對於上面這個例子,我們可以這麼來改,從而避免出現可見性問題。


    總的來說,記憶體屏障的作用可以通過防止 CPU 對記憶體的亂序訪問來保證共享資料在多執行緒並行執行下的可見性,但是這個屏障怎麼來加呢?回到最開始我們講 volatile 關鍵字的程式碼,這個關鍵字會生成一個 Lock 的彙編指令,這個指令其實就相當於實現了一種記憶體屏障。


    面試官: 說到volatile,那麼你能說說什麼是JMM嗎?


    小黃:JMM全稱是java記憶體模型,因為導致可見性的根本原因是快取和重排序,而JMM則合理的禁用了快取和禁用了重排序,所以他最核心的價值就是解決了可見性和有序性。


    面試官:JMM是如何解決可見性和有序性的?


    小黃:JMM基於CPU層面提供的記憶體屏障指令來限制編譯器的重排序,從而解決併發問題。


    JMM提供了一些禁止快取和禁止重排序的方法,比如上面說的volatile,以及synchronize(後續會單獨寫一篇關於synchronize的文章),final;


    JMM 如何解決順序一致性問題 

    重排序問題

    為了提高程式的執行效能,編譯器和處理器都會對指令做重排序,其中處理器的重排序在前面已經分析過了。所謂的重排序其實就是指執行的指令順序。編譯器的重排序指的是程式編寫的指令在編譯之後,指令可能會產生重排序來優化程式的執行效能。從原始碼到最終執行的指令,可能會經過三種重排序。

    一文吃透Volatile,征服面試官


    2 和 3 屬於處理器重排序。這些重排序可能會導致可見性問題。編譯器的重排序,JMM 提供了禁止特定型別的編譯器重排序。


    處理器重排序,JMM 會要求編譯器生成指令時,會插入記憶體屏障來禁止處理器重排序,當然並不是所有的程式都會出現重排序問題。


    編譯器的重排序和 CPU 的重排序的原則一樣,會遵守資料依賴性原則,編譯器和處理器不會改變存在資料依賴關係的兩個操作的執行順序,比如下面的程式碼,


      a=1; b=a; 複製程式碼
      複製程式碼
      a=1;a=2;複製程式碼
      複製程式碼
      a=b;b=1;複製程式碼


      這三種情況在單執行緒裡面如果改變程式碼的執行順序,都會導致結果不一致,所以重排序不會對這類的指令做優化。

      這種規則也成為 as-if-serial。不管怎麼重排序,對於單個執行緒來說執行結果不能改變。比如

        int a=2; //1複製程式碼
        複製程式碼
        int b=3; //2複製程式碼
        複製程式碼
        int rs=a*b; //3複製程式碼

        1 和 3、2 和 3 存在資料依賴,所以在最終執行的指令中,3 不能重排序到 1 和 2 之前,否則程式會報錯。由於 1 和 2不存在資料依賴,所以可以重新排列 1 和 2 的順序。


        JMM 層面的記憶體屏障


        為了保證記憶體可見性,Java 編譯器在生成指令序列的適當位置會插入記憶體屏障來禁止特定型別的處理器的重排序,在 JMM 中把記憶體屏障分為四類

        一文吃透Volatile,征服面試官

        HappenBefore,它的意思表示的是前一個操作的結果對於後續操作是可見的,所以它是一種表達多個執行緒之間對於記憶體的可見性。


        所以我們可以認為在 JMM 中,如果一個操作執行的結果需要對另一個操作課件,那麼這兩個操作必須要存在happens-before 關係。


        這兩個操作可以是同一個執行緒,也可以是不同的執行緒,JMM 中有哪些方法建立 happen-before 規則 呢?


        程式順序規則 

        1. 一個執行緒中的每個操作,happens-before 於該執行緒中的任意後續操作; 可以簡單認為是 as-if-serial。單個執行緒中的程式碼順序不管怎麼變,對於結果來說是不變的順序規則表示 1 happenns-before 2;

          3 happens-before 4

          一文吃透Volatile,征服面試官

        2. volatile 變數規則,對於 volatile 修飾的變數的寫的操作,一定 happen-before 後續對於 volatile 變數的讀操作;根據 volatile 規則,2 happens before 3

          一文吃透Volatile,征服面試官

        3. 傳遞性規則,如果 1 happens-before 2; 2 happens-before 3; 那麼傳遞性規則表示: 1 happens-before 3;


        4. start 規則,如果執行緒 A 執行操作 ThreadB.start(),那麼執行緒 A 的 ThreadB.start()操作 happens-before 執行緒 B 中的任意操作


          一文吃透Volatile,征服面試官

        5. join 規則,如果執行緒 A 執行操作 ThreadB.join()併成功返回,那麼執行緒 B 中的任意操作 happens-before 於執行緒A 從 ThreadB.join()操作成功返回。

          一文吃透Volatile,征服面試官

        6. 監視器鎖的規則,對一個鎖的解鎖,happens-before 於隨後對這個鎖的加鎖

          一文吃透Volatile,征服面試官

               假設 x 的初始值是 10,執行緒 A 執行完程式碼塊後 x 的值會變成 12(執 行完自動釋放鎖),執行緒 B 進入程式碼塊時,能夠看到執行緒 A 對 x 的寫操作,也就是執行緒 B 能夠看到 x==12。


        面試官(os):一文吃透Volatile,征服面試官一文吃透Volatile,征服面試官一文吃透Volatile,征服面試官一文吃透Volatile,征服面試官一文吃透Volatile,征服面試官這麼厲害?用不起用不起,回去等通知吧!


        小黃心裡一萬隻草泥馬奔騰而過。。。。。。一文吃透Volatile,征服面試官一文吃透Volatile,征服面試官一文吃透Volatile,征服面試官一文吃透Volatile,征服面試官一文吃透Volatile,征服面試官一文吃透Volatile,征服面試官

        版權宣告:本站原創文章,於2019-10-08,由 TopJavaer 發表。 轉載請註明來源 juejin.im/post/5d9c8a…,謝謝。

        相關文章