Java 語言中的 volatile 變量能夠被看做是一種 「程度較輕的 synchronized」;與 ynchronized 塊相比,volatile 變量所需的編碼較少,而且運行時開銷也較少,可是它所 能實現的功能也僅是 synchronized 的一部分。本文介紹了幾種有效使用 volatile 變量 的模式,並強調了幾種不適合使用 volatile 變量的情形。
鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互 斥即一次只容許一個線程持有某個特定的鎖,所以可以使用該特性實現對共享數據的協調訪問 協議,這樣,一次就只有一個線程可以使用該共享數據。可見性要更加複雜一些,它必須確 保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的 —— 若是沒有 同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這 將引起許多嚴重問題。
Volatile 變量具備 synchronized 的可見性特性,可是不具有原子特性。這就是說 線程可以自動發現 volatile 變量的最新值。Volatile 變量可用於提供線程安全,可是 只能應用於很是有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有 約束。所以,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具備與多個變量相 關的不變式(Invariants)的類(例如 「start <=end」)。
出於簡易性或可伸縮性的考慮,您可能傾向於使用 volatile 變量而不是鎖。當使用 volatile變量而非鎖時,某些習慣用法(idiom)更加易於編碼和閱讀。此外,volatile 變量不會像鎖那樣形成線程阻塞,所以也不多形成可伸縮性問題。在某些狀況下,若是讀操 做遠遠大於寫操做,volatile 變量還能夠提供優於鎖的性能優點。
您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線
程安全,必須同時知足下面兩個條件:html
- 對變量的寫操做不依賴於當前值。 - 該變量沒有包含在具備其餘變量的不變式中。
實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀 態,包括變量的當前狀態。
第一個條件的限制使 volatile 變量不能用做線程安全計數器。雖然增量操做(x++) 看上去相似一個單獨操做,實際上它是一個由讀取-修改-寫入操做序列組成的組合操做,必 須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操做須要使 x 的值 在操做期間保持不變,而 volatile 變量沒法實現這點。(然而,若是將值調整爲只從單個線 程寫入,那麼能夠忽略第一個條件。)
大多數編程情形都會與這兩個條件的其中之一衝突,使得 volatile 變量不能像 synchronized 那樣廣泛適用於實現線程安全。清單 1 顯示了一個非線程安全的數值範圍類。 它包含了一個不變式 —— 下界老是小於或等於上界。
清單 1. 非線程安全的數值範圍類java
@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 類型是沒法實現這一目的的。
使用 volatile 變量的主要緣由是其簡易性:在某些情形下,使用 volatile 變量 要比使用相應的鎖簡單得多。使用 volatile 變量次要緣由是其性能:某些狀況下,volatile 變量同步機制的性能要優於鎖。
很難作出準確、全面的評價,例如 「X 老是比 Y 快」,尤爲是對 JVM 內在的操做而 言。(例如,某些狀況下 VM 也許可以徹底刪除鎖機制,這使得咱們難以抽象地比較 volatile 和 synchronized 的開銷。)就是說,在目前大多數的處理器架構上,volatile 讀操做開 銷很是低 —— 幾乎和非 volatile 讀操做同樣。而 volatile 寫操做的開銷要比非volatile 寫操做多不少,由於要保證可見性須要實現內存界定(Memory Fence),即使如此,volatile 的總開銷仍然要比鎖獲取低。
volatile 操做不會像鎖同樣形成阻塞,所以,在可以安全使用 volatile 的狀況下, volatile 能夠提供一些優於鎖的可伸縮特性。若是讀操做的次數要遠遠超過寫操做,與鎖相 比,volatile 變量一般可以減小同步的性能開銷。
不少併發性專家事實上每每引導用戶遠離 volatile 變量,由於使用它們要比使用鎖 更加容易出錯。然而,若是謹慎地遵循一些良好定義的模式,就可以在不少場合內安全地使用 volatile 變量。要始終牢記使用 volatile 的限制 —— 只有在狀態真正獨立於程序內其餘 內容時才能使用 volatile —— 這條規則可以避免將這些模式擴展到不安全的用例。
也許實現 volatile 變量的規範使用僅僅是使用一個布爾狀態標誌,用於指示發生了一 個重要的一次性事件,例如完成初始化或請求停機。
不少應用程序包含了一種控制結構,形式爲 「在尚未準備好中止程序時再執行一些工 做」,如清單 2 所示:
清單 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)。此外,還須要某些原
子狀態轉換機制,例如原子變量。數組
缺少同步會致使沒法實現可見性,這使得肯定什麼時候寫入對象引用而不是原語值變得更 加困難。在缺少同步的狀況下,可能會遇到某個對象引用的更新值(由另外一個線程寫入)和 該對象狀態的舊值同時存在。(這就是形成著名的雙重檢查鎖定(double-checked-locking) 問題的根源,其中對象引用在沒有同步的狀況下進行讀操做,產生的問題是您可能會看到一 個更新的引用,可是仍然會經過該引用看到不徹底構造的對象)。
實現安全發佈對象的一種技術就是將對象引用定義爲 volatile 類型。清單 3 展現 了一個示例,其中後臺線程在啓動階段從數據庫加載一些數據。其餘代碼在可以利用這些數 據時,在使用以前將檢查這些數據是否曾經發布過。
清單 3. 將 volatile 變量用於一次性安全發佈安全
public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // do lots of stuff theFlooble = new Flooble(); // this is the only write to theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // do some stuff... // use the Flooble, but only if it is ready if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } }
若是 theFlooble 引用不是 volatile 類型,doWork() 中的代碼在解除對 theFlooble 的引用時,將會獲得一個不徹底構造的 Flooble。
該模式的一個必要條件是:被髮布的對象必須是線程安全的,或者是有效的不可變對 象(有效不可變意味着對象的狀態在發佈以後永遠不會被修改)。volatile 類型的引用能夠 確保對象的發佈形式的可見性,可是若是對象的狀態在發佈後將發生更改,那麼就須要額外的 同步。
安全使用 volatile 的另外一種簡單模式是:按期 「發佈」 觀察結果供程序內部使用。 例如,假設有一種環境傳感器可以感受環境溫度。一個後臺線程可能會每隔幾秒讀取一次該 傳感器,並更新包含當前文檔的 volatile 變量。而後,其餘線程能夠讀取這個變量,從而 隨時可以看到最新的溫度值。
使用該模式的另外一種應用程序就是收集程序的統計信息。清單 4 展現了身份驗證機制 如何記憶最近一次登陸的用戶的名字。將反覆使用 lastUser 引用來發布值,以供程序的其 他部分使用。
清單 4. 將 volatile 變量用於多個獨立觀察結果的發佈架構
public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } }
該模式是前面模式的擴展;將某個值發佈以在程序內的其餘地方使用,可是與一次性 事件的發佈不一樣,這是一系列獨立事件。這個模式要求被髮布的值是有效不可變的 —— 即值 的狀態在發佈後不會更改。使用該值的代碼須要清楚該值可能隨時發生變化。
volatile bean 模式適用於將 JavaBeans 做爲「榮譽結構」使用的框架。在volatile bean 模式中,JavaBean 被用做一組具備 getter 和/或 setter 方法 的獨立屬性的容器。 volatile bean 模式的基本原理是:不少框架爲易變數據的持有者(例如 HttpSession)提 供了容器,可是放入這些容器中的對象必須是線程安全的。
在 volatile bean 模式中,JavaBean 的全部數據成員都是 volatile 類型的,而且 getter 和 setter 方法必須很是普通 —— 除了獲取或設置相應的屬性外,不能包含任何邏輯。 此外,對於對象引用的數據成員,引用的對象必須是有效不可變的。(這將禁止具備數組值的屬 性,由於當數組引用被聲明爲 volatile 時,只有引用而不是數組自己具備 volatile 語義)。 對於任何 volatile 變量,不變式或約束都不能包含 JavaBean 屬性。清單 5 中的示例展現 了遵照 volatile bean 模式的 JavaBean:
清單 5. 遵照 volatile bean 模式的 Person 對象併發
@ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
前面幾節介紹的模式涵蓋了大部分的基本用例,在這些模式中使用 volatile 很是有用 而且簡單。這一節將介紹一種更加高級的模式,在該模式中,volatile 將提供性能或可伸縮 性優點。
volatile 應用的的高級模式很是脆弱。所以,必須對假設的條件仔細證實,而且這些模 式被嚴格地封裝了起來,由於即便很是小的更改也會損壞您的代碼!一樣,使用更高級的volatile 用例的緣由是它可以提高性能,確保在開始應用高級模式以前,真正肯定須要實現這種性能獲益。 須要對這些模式進行權衡,放棄可讀性或可維護性來換取可能的性能收益 —— 若是您不須要提高 性能(或者不可以經過一個嚴格的測試程序證實您須要它),那麼這極可能是一次糟糕的交易, 由於您極可能會得不償失,換來的東西要比放棄的東西價值更低。
目前爲止,您應該瞭解了 volatile 的功能還不足以實現計數器。由於 ++x 其實是三 種操做(讀、添加、存儲)的簡單組合,若是多個線程湊巧試圖同時對 volatile 計數器執行 增量操做,那麼它的更新值有可能會丟失。
然而,若是讀操做遠遠超過寫操做,您能夠結合使用內部鎖和 volatile 變量來減小公共 代碼路徑的開銷。清單 6 中顯示的線程安全的計數器使用 synchronized 確保增量操做是原子 的,並使用 volatile 保證當前結果的可見性。若是更新不頻繁的話,該方法可實現更好的性能, 由於讀路徑的開銷僅僅涉及 volatile 讀操做,這一般要優於一個無競爭的鎖獲取的開銷。
清單 6. 結合使用 volatile 和 synchronized 實現 「開銷較低的讀-寫鎖」框架
@ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } }
之因此將這種技術稱之爲 「開銷較低的讀-寫鎖」 是由於您使用了不一樣的同步機制進行讀 寫操做。由於本例中的寫操做違反了使用 volatile 的第一個條件,所以不能使用 volatile 安全地實現計數器 —— 您必須使用鎖。然而,您能夠在讀操做中使用 volatile 確保當前值的 可見性,所以可使用鎖進行全部變化的操做,使用 volatile 進行只讀操做。其中,鎖一次 只容許一個線程訪問值,volatile 容許多個線程執行讀操做,所以當使用 volatile 保證讀 代碼路徑時,要比使用鎖執行所有代碼路徑得到更高的共享度 —— 就像讀-寫操做同樣。然而, 要隨時牢記這種模式的弱點:若是超越了該模式的最基本應用,結合這兩個競爭的同步機制將 變得很是困難。
與鎖相比,Volatile 變量是一種很是簡單但同時又很是脆弱的同步機制,它在某些狀況 下將提供優於鎖的性能和伸縮性。若是嚴格遵循 volatile 的使用條件 —— 即變量真正獨立於 其餘變量和本身之前的值 —— 在某些狀況下可使用 volatile 代替 synchronized 來簡化 代碼。然而,使用 volatile 的代碼每每比使用鎖的代碼更加容易出錯。本文介紹的模式涵蓋 了可使用 volatile 代替 synchronized 的最多見的一些用例。遵循這些模式(注意使用 時不要超過各自的限制)能夠幫助您安全地實現大多數用例,使用 volatile 變量得到更佳性 能。
原文連接
若是有侵權,立刻刪除性能