Java多線程(3) Volatile的實現原理

Volatile變量

程序設計中,尤爲是在C語言C++C#Java語言中,使用volatile關鍵字聲明的變量對象一般擁有和優化和(或)多線程相關的特殊屬性。一般,volatile關鍵字用來阻止(僞)編譯器對某些其認爲沒法「被代碼自己」改變的代碼(變量/對象)進行優化。如在C語言中,volatile關鍵字能夠用來提醒編譯器它後面所定義的變量隨時有可能改變,所以編譯後的程序每次須要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數據。若是沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,若是這個變量由別的程序更新了的話,將出現不一致的現象。 在C環境中,volatile關鍵字的真實定義和適用範圍常常被誤解。雖然C++、C#和Java都從C中神祕地「繼承」了volatile,在這些編程語言中volatile的用法和語義卻截然不同。【維基百科】java

 

通俗點講解上面這句話,意思就是,編譯器爲了快速讀寫,會將數據放到寄存器緩存中,而使用了volatile修飾之後,告訴編譯器,不要對該變量進行優化,每次讀取都從內存中去讀取。編程

 

鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互斥即一次只容許一個線程持有某個特定的鎖,所以可以使用該特性實現對共享數據的協調訪問協議,這樣,一次就只有一個線程可以使用該共享數據。可見性要更加複雜一些,它必須確保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的 —— 若是沒有同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這將引起許多嚴重問題。緩存

 

Volatile 變量具備 synchronized 的可見性特性,可是不具有原子特性。這就是說線程可以自動發現 volatile 變量的最新值。Volatile 變量可用於提供線程安全,可是隻能應用於很是有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。所以,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具備與多個變量相關的不變式(Invariants)的類安全

 

 

1.1 正確使用 volatile 變量的條件

您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:多線程

  • 對變量的寫操做不依賴於當前值。
  • 該變量沒有包含在具備其餘變量的不變式中。

第一個條件的限制使 volatile 變量不能用做線程安全計數器。雖然增量操做(x++)看上去相似一個單獨操做,實際上它是一個由讀取-修改-寫入操做序列組成的組合操做,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操做須要使 x 的值在操做期間保持不變,而 volatile 變量沒法實現這點。(然而,若是將值調整爲只從單個線程寫入,那麼能夠忽略第一個條件。)架構

清單 1. 非線程安全的數值範圍類
@NotThreadSafe 
public class NumberRange {
    private int lower, upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }

    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}

這種方式限制了範圍的狀態變量,所以將 lower 和 upper 字段定義爲 volatile 類型不可以充分實現類的線程安全;從而仍然須要使用同步。不然,若是湊巧兩個線程在同一時間使用不一致的值執行 setLower 和 setUpper 的話,則會使範圍處於不一致的狀態。例如,若是初始狀態是(0, 5),同一時間內,線程 A 調用 setLower(4) 而且線程 B 調用 setUpper(3),顯然這兩個操做交叉存入的值是不符合條件的,那麼兩個線程都會經過用於保護不變式的檢查,使得最後的範圍值是 (4, 3) —— 一個無效值。至於針對範圍的其餘操做,咱們須要使 setLower()和 setUpper() 操做原子化 —— 而將字段定義爲 volatile 類型是沒法實現這一目的的。  併發

   

1.2  性能考慮

很難作出準確、全面的評價,例如 「X 老是比 Y 快」,尤爲是對 JVM 內在的操做而言。(例如,某些狀況下 VM 也許可以徹底刪除鎖機制,這使得咱們難以抽象地比較 volatile 和 synchronized 的開銷。)就是說,在目前大多數的處理器架構上,volatile 讀操做開銷很是低 —— 幾乎和非 volatile 讀操做同樣。而 volatile 寫操做的開銷要比非 volatile 寫操做多不少,由於要保證可見性須要實現內存界定(Memory Fence),即使如此,volatile 的總開銷仍然要比鎖獲取低。編程語言

 

不少併發性專家事實上每每引導用戶遠離 volatile 變量,由於使用它們要比使用鎖更加容易出錯。然而,若是謹慎地遵循一些良好定義的模式,就可以在不少場合內安全地使用 volatile 變量。要始終牢記使用 volatile 的限制 —— 只有在狀態真正獨立於程序內其餘內容時才能使用 volatile —— 這條規則可以避免將這些模式擴展到不安全的用例。性能

 

1.3 正確使用 volatile 的模式

清單 2. 將 volatile 變量做爲狀態標誌使用
volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

 

極可能會從循環外部調用 shutdown() 方法 —— 即在另外一個線程中 —— 所以,須要執行某種同步來確保正確實現 shutdownRequested 變量的可見性。(可能會從 JMX 偵聽程序、GUI 事件線程中的操做偵聽程序、經過 RMI 、經過一個 Web 服務等調用)。然而,使用synchronized 塊編寫循環要比使用清單 2 所示的 volatile 狀態標誌編寫麻煩不少。因爲 volatile 簡化了編碼,而且狀態標誌並不依賴於程序內任何其餘狀態,所以此處很是適合使用 volatile。優化

這種類型的狀態標記的一個公共特性是:一般只有一種狀態轉換;shutdownRequested 標誌從 false 轉換爲 true,而後程序中止。這種模式能夠擴展到來回轉換的狀態標誌,可是隻有在轉換週期不被察覺的狀況下才能擴展(從 false 到 true,再轉換到 false)。此外,還須要某些原子狀態轉換機制,例如原子變量。

 

 

與鎖相比,Volatile 變量是一種很是簡單但同時又很是脆弱的同步機制,它在某些狀況下將提供優於鎖的性能和伸縮性。若是嚴格遵循 volatile 的使用條件 —— 即變量真正獨立於其餘變量和本身之前的值 —— 在某些狀況下可使用 volatile 代替 synchronized 來簡化代碼。然而,使用 volatile 的代碼每每比使用鎖的代碼更加容易出錯。本文介紹的模式涵蓋了可使用 volatile 代替 synchronized 的最多見的一些用例。遵循這些模式(注意使用時不要超過各自的限制)能夠幫助您安全地實現大多數用例,使用 volatile 變量得到更佳性能。

相關文章
相關標籤/搜索