你真的懂volatile嗎

寫這篇文章的目的,是在交流中發現有的同窗對於volatile的happens-before規則並不太清楚,本文針對對於JMM內存模型的原子性、有序性、可見性等概念有必定了解但對於volatile理解有些模糊的同窗,大約花費7分鐘左右時間緩存

由一個問題開始:ReentrantLock是如何實現與synchronized鎖相同的內存可見性語義的?即:synchronized鎖內操做的共享變量值修改在鎖被釋放後可以保證被其餘線程當即看到,ReentrantLock鎖可以保證嗎,是如何保證的?bash

固然可以保證,不然就談不上是同步鎖,如何保證的正是本文要談的內容微信

誤區:有些同窗認爲volatile修飾的共享變量寫操做僅保證當前變量的內存可見性,刷新當前變量所在緩存行回內存,同時由緩存一致性協議invalid其餘緩存

幾個必要的名次解釋

  • 原子性:在Java中,32位JVM對32位基本數據類型變量的讀取和賦值操做是原子性操做,即這些操做是不可被中斷的,要麼所有執行,要麼所有不執行(volatile另外保證了在此環境下64位long和double類型變量讀取和賦值操做的原子性,注意是讀取和賦值單步操做,而不包含自增這種須要被拆解爲多步操做的計算)
  • 指令重排序:編譯器階段和處理器階段的指令重排序,不存在數據依賴性的指令能夠發生指令重排序,重排序可能致使程序錯誤,以下圖,1和2之間、3和4之間均可能發生重排序,4有多步操做,處理器有可能出於優化考慮,先計算a*a,再判斷if條件,而後決定是否給i賦值,若是沒有同步機制,執行結果極可能是錯的
class ReorderExample {  
    int a = 0;  
    boolean flag = false;  
  
    public void writer() {  
        a = 1;          // 1  
        flag = true;    // 2  
    }  
  
    public void reader() {  
        if (flag) {            // 3  
            int i = a * a; // 4  
        }  
    }  
}  
複製代碼
  • cpu緩存模型 多線程

    cpu cache模型
    1)cpu尋找數據流程:L1 cache->L2 cache->L3 cache->主內存(其實L1以前還有store/load buffer,下文會有簡單介紹
    2)緩存行:緩存是由緩存行組成的,通常一行緩存行有64字節,cpu存取緩存是以緩存行爲最小單位操做的
    3)因爲緩存結構的存在,若是沒有完善的緩存一致性協議保障,就會致使多線程的內存可見性問題

  • JVM內存模型架構

    本地內存vs主內存
    1)因爲線程在工做內存(即圖中的本地內存)中存在變量副本(包含共享變量),而致使在沒有內存同步(緩存一致性協議)的前提下,不一樣線程對於共享變量操做的執行結果是不肯定的
    2)線程工做內存只是JVM的概念模型(爲了適配不一樣的機器結構和操做系統),JAVA線程藉助了底層操做系統線程實現,一個JVM線程對應一個操做系統線程,線程的工做內存實際上是cpu寄存器和高速緩存的抽象

  • 緩存一致性協議 解決緩存不一致問題,一般來講有如下2種方法:
    1)經過在總線加LOCK#鎖的方式(效率過低
    2)經過緩存一致性協議(核心思想:當CPU寫數據時,若是發現操做的變量是共享變量,且在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,後續當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,就會從內存從新讀取
    3)緩存一致性協議不能徹底保證內存可見性,由於爲了提高性能,存在store buffer(存儲緩存)、invalidate queue(失效隊列)等結構,致使內存可見性受影響,由此引出了內存屏障
    4)不深究細節(細節我也不太懂),具體請搜索MESI及爲何須要內存屏障app

  • 內存屏障性能

    內存屏障

    1)內存屏障:讓一個CPU處理單元中的內存狀態對其它處理單元可見的一項技術,阻止讀寫內存動做的重排序
    2)寫內存屏障(Store Memory Barrier):處理器將當前store buffer(存儲緩存)的值寫回主存,以阻塞的方式
    3)讀內存屏障(Load Memory Barrier):處理器處理invalidate queue(失效隊列),以阻塞的方式
    注:上圖爲JMM內存屏障抽象規範,JVM會根據不一樣的操做系統插入不一樣的指令以達成想要的內存屏障效果
    4)LoadLoad:確保Load1所要讀入的數據可以在被Load2和後續的load指令訪問前讀入。一般能執行預加載指令或/和支持亂序處理的處理器中須要顯式聲明Loadload屏障,由於在這些處理器中正在等待的加載指令可以繞過正在等待存儲的指令, 而對於老是能保證處理順序的處理器上,設置該屏障至關於無操做
    5)StoreStore:確保Store1的數據在Store2以及後續Store指令操做相關數據以前對其它處理器可見(例如向主存刷新數據)。一般狀況下,若是處理器不能保證從寫緩衝或/和緩存向其它處理器和主存中按順序刷新數據,那麼它須要使用StoreStore屏障
    6)LoadStore:確保Load1的數據在Store2和後續Store指令被刷新以前讀取。在等待Store指令能夠越過loads指令的亂序處理器上須要使用LoadStore屏障
    7)StoreLoad Barriers:確保Store1的數據在被Load2和後續的Load指令讀取以前對其餘處理器可見。StoreLoad屏障能夠防止一個後續的load指令不正確的使用了Store1的數據,而不是另外一個處理器在相同內存位置寫入一個新數據
    8)
    內存屏障在不一樣機器架構上的具體實現
    上圖可見,X86僅對StoreLoad屏障作了操做,其餘屏障底層實現均爲no-op
    9)在x86架構下,volatile寫操做會在彙編代碼(可開啓虛擬機-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:MaxInlineSize=0參數查看彙編代碼)中插入一條StoreLoad屏障lock addl $0x0,(%rsp)

總結:多線程亂序狀況彙總優化

  • 指令重排序致使的亂序,經過內存屏障禁止指令重排序解決
  • 現代處理器採用的內部緩存技術致使數據的變化不能及時反映在主存所帶來的亂序,經過緩存一致性協議和內存屏障解決

爲了保證多線程程序執行的正確性,JMM定義了happens-before規則,重排序須要遵照happens-before規則spa

happens-before規則操作系統

  • 程序次序規則:一段代碼在單線程中執行的結果是有序的(在只有單線程執行程序的條件下,雖然可能進行了指令重排序,線程最終執行的結果與程序順序執行的結果仍然是一致的,由於指令重排序只會對不存在數據依賴的指令進行重排序,所以,在單個線程執行條件下,程序看起來是有序執行的,但這個規則沒法保證程序在多線程執行條件下的正確性
  • 監視器鎖規則:對一個監視器的解鎖 happens-before 於每一個後續對同一監視器的加鎖(不管在單線程仍是多線程執行條件下,同一個鎖若是處於被鎖定的狀態,必須等待持有鎖線程先釋放鎖,其餘線程才能再次競爭加鎖
  • volatile變量規則:對 volatile域的寫入操做 happens-before 於後續對同一 volatile的讀操做(線程老是能當即讀取到本線程或者其餘線程對於同一個volatile變量的最新的寫入值
  • 傳遞性:若是 A happens-before 於 B,且 B happens-before C,則 A happens-before C

volatile底層實現正是藉助內存屏障和緩存一致性協議保障了happens-before規則

回到最初的問題,ReentrantLock如何實現鎖的內存可見性語義?

  • ReentrantLock同步機制須要先lock()獲取鎖,而後進入同步代碼塊,最後在finally塊調用unlock()方法後退出同步
  • lock()/unlock()操做均藉助AbstractQueuedSynchronizer中的volatile int state變量實現
  • lock()底層調用Unsafe類的compareAndSwapInt(),該操做爲原子操做,且和volatile具有相同的讀寫語義,所以其餘線程能夠當即看到state值的變化
  • unlock()時,state減1,int變量的賦值操做爲原子操做,且爲volatile寫,所以其餘線程能夠當即看到state變化
  • 根據以前的happens-before規則(一、三、4條),同步塊之中的內存變化也能夠被其餘線程當即看到,由此實現了鎖的內存可見性語義
  • 同理,多線程環境下,下圖代碼reader()能夠保障x爲42
class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}
複製代碼
歡迎關注個人微信公衆號

68號小喇叭
相關文章
相關標籤/搜索