volatile的理解

一路上秋風發表於2020-11-04

volatile關鍵字的作用

volatile用於修飾變數

  • 保證變數的記憶體可見性
  • 保證原子性
  • 禁止指令重排序

JMM模型

Java記憶體模型(Java Memory Model)是一種抽象的,本身不存在的,用來一組規範,來定製程式中各個變數的訪問方式.

JMM關於同步的規定

  • 執行緒解鎖前,必須把共享變數的值寫回主記憶體.
  • 執行緒加鎖前,必須讀取主記憶體中變數的最新值到自己工作記憶體.
  • 加鎖與解鎖必須使用同一把鎖.
    由於JVM執行程式的載體是執行緒,而JVM建立每個執行緒時都會為該執行緒分配一塊私有的工作記憶體(也稱為棧記憶體).JMM規定所有變數都儲存在主記憶體中,主記憶體是共享區域,所有執行緒都可以訪問.但執行緒對變數的操作(讀取賦值等)都必須在工作記憶體中進行,首先從主記憶體中讀取變數到工作記憶體中,然後對該變數進行操作,操作完成把該變數寫回主記憶體.各個執行緒的工作記憶體中儲存著主記憶體變數的拷貝副本.不同執行緒無法訪問對方的工作記憶體,因此必須通過主記憶體來完成.
    JMM執行緒操作變數

可見性

不同執行緒各自的記憶體空間裡的變數是不能互相訪問的,稱為不可見性.
如果A執行緒修改了從主記憶體拷貝的一個變數,還未寫回主記憶體,而B執行緒從主記憶體中讀取了該變數,那麼此變數的值就是A執行緒修改前的值,這就造成了AB兩個執行緒的不可見問題.

原子性

原子性表示某個操作是不可拆分的.
比如i++操作,在底層被拆分成了三個操作:執行getfiled拿到原始的i,執行iadd進行+1操作,執行putfield操作把累加後的值寫回.
如果有變數i初始值為0,執行緒A在工作記憶體中執行i++操作,首先拿到原始i,但還未執行+1操作時,執行緒B得到了cpu時間,執行緒A阻塞了.執行緒B在工作記憶體中也執行了i++操作,然後把i的值1寫回了主記憶體.但是此時執行緒A不會再返回去讀取主記憶體的i值,此時A執行緒工作記憶體中的i值還是0,A執行緒繼續執行i++操作,然後把i的值1寫回到主記憶體.此時主記憶體的i值就是1,B執行緒的操作就被A執行緒覆蓋了.

有序性

為了儘可能的減少記憶體速度遠慢於cpu速度所帶來的cou閒置問題,虛擬機器會按照自己的特定規則,將程式的編寫順序打亂執行,以儘可能的充分利用cpu,提高執行效率.這個前提是亂序執行不會導致程式結果出錯.在單執行緒環境下,可以保證程式結果正確,但是在多執行緒環境下,重排序後的程式結果正確性是無法保證的.

volatile的實現原理

記憶體屏障(Memory Barrier),是一個cpu指令,作用有兩個:

  • 保證特定操作的執行順序.
  • 保證某些變數的記憶體可見性.
    編譯器和處理器都可以執行指令重排序優化,如果在指令間插入一條記憶體屏障則會告訴編譯器和cpu,不管什麼指令都不能和這條記憶體屏障進行指令重排序,也就是通過插入記憶體屏障禁止在記憶體屏障的前後進行指令重排序.記憶體屏障另一個作用就是強制重新整理出cpu的各種快取資料,因此cpu上的所有執行緒都能讀取到這些資料的最新值.
    在這裡插入圖片描述

volatile的使用場景

  • 單例模式DCL(雙重檢查鎖)
class SingletonDemo2 {
    public volatile static SingletonDemo2 instance = null;

    private SingletonDemo2() {

    }

    public static SingletonDemo2 getInstance() {
        if (null == instance) {
            synchronized (SingletonDemo2.class) {
                if (null == instance) {
                    instance = new SingletonDemo2();
                }
            }
        }
        return instance;
    }
}

單例物件必須加volatile關鍵字的原因:
DCL不一定執行緒安全,因為有指令重排序的存在,加入volatile可以禁止指令重排序.初始化一個單例物件底層可分為以下步驟:

  • 分配記憶體空間
  • 初始化物件
  • 設定物件指向剛分配的記憶體地址
    步驟二和步驟三不存在資料依賴關係,而且指令重排後在單執行緒情況下不會改變程式的結果,因此這種重排優化是允許的.重排後會導致A執行緒拿到的instance不為null,但是此時instance未必已經完成初始化,也就造成了執行緒安全問題.

相關文章