Java 語言中的 volatile 變數可視為一種「程度較輕的 synchronized」;與 synchronized 區塊相比,volatile 變數所需編碼較少、執行時開銷較少,但能實現功能僅是 synchronized 一部分。本文介紹幾種有效使用 volatile 變數模式,並強調幾種不適合使用 volatile 變數情形。
鎖提供兩種主要特性:互斥與可見性。互斥即一次只允許一執行緒持有特定鎖,可用此特性實現對共享資料協調訪問協議,使一次只有一執行緒能使用該共享資料。可見性較複雜,必須確保釋放鎖前對共享資料做出的更改,對隨後獲得該鎖另一執行緒可見——若無同步機制提供此可見性保證,執行緒看到的共享變數可能是修改前值或不一致值,引發嚴重問題。
volatile 變數具備 synchronized 的可見性特性,但不具原子特性。這意味著執行緒能自動發現 volatile 變數最新值。volatile 變數可用於提供執行緒安全,但只能應用於非常有限用例:多個變數間或某變數當前值與修改後值間沒有約束。因此單獨使用 volatile 仍不足以實現計數器、互斥鎖或任何具有與多變數相關不變式(Invariants)的類(如 start <= end)。
出於簡易性或可伸縮性考慮,您可能傾向用 volatile 變數而非鎖。用 volatile 而非鎖時,某些習慣用法更易編碼閱讀。此外 volatile 變數不會像鎖那樣造成執行緒阻塞,故很少造成可伸縮性問題。某些情況下,若讀操作遠超寫操作,volatile 變數還能提供優於鎖的效能優勢。
只能在有限情形下用 volatile 變數替代鎖。要使 volatile 變數提供理想執行緒安全,必須同時滿足兩條件:
實際上,這些條件表明可寫入 volatile 變數的有效值獨立於任何程式狀態,含變數當前狀態。
第一條件限制使 volatile 變數不能用作執行緒安全計數器。雖增量操作(x++)看似單一操作,實際是讀取-修改-寫入操作序列組成的組合操作,必須以原子方式執行,而 volatile 無法提供必須原子特性。實現正確操作需使 x 值在操作期間保持不變,volatile 變數無法做到。(然而,若將值調整為只從單一執行緒寫入,可忽略第一條件。)
大多數編程情形會與這兩條件之一衝突,使 volatile 變數不能像 synchronized 那樣普遍適用於實現執行緒安全。清單 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 無法達成。
使用 volatile 變數主要原因是簡易性:某些情形下用 volatile 比相應鎖簡單得多。次要原因是效能:某些情況下 volatile 同步機制效能優於鎖。
難做出準確全面評價如「X 總比 Y 快」,尤其對 JVM 內在操作。(例如某些情況 VM 或許能完全刪除鎖機制,使難以抽象比較 volatile 與 synchronized 開銷。)說來,目前大多數處理器架構上,volatile 讀操作開銷極低——幾乎與非 volatile 讀相同。而 volatile 寫操作開銷比非 volatile 寫多很多,因保證可見性需實現記憶體屏障,即便如此 volatile 總開銷仍比鎖獲取低。
volatile 操作不會像鎖造成阻塞,故在能安全使用 volatile 情況下,volatile 可提供優於鎖的可伸縮特性。若讀操作次數遠超寫操作,與鎖相比 volatile 變數通常能減少同步效能開銷。
許多並發專家事實上常引導用戶遠離 volatile 變數,因使用它們比用鎖更容易出錯。然而若謹慎遵循良好定義模式,能在許多場合安全使用 volatile 變數。始終牢記 volatile 限制——只有狀態真正獨立於程式內其他內容時才能用 volatile——此規則能避免將模式擴展到不安全用例。
或許實現 volatile 變數規範用法僅是使用一個布林狀態標誌,指示發生重要一次性事件,如完成初始化或請求停機。
許多應用程式包含控制結構,形式為「在還沒準備好停止程式時再執行一些工作」,如清單 2:
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
很可能從迴圈外部調用 shutdown()——即另一執行緒——故需某種同步確保正確實現 shutdownRequested 可見性。(可能從 JMX 偵聽程式、GUI 事件執行緒操作偵聽程式、透過 RMI、透過 Web Service 調用)。然而用 synchronized 區塊寫迴圈比用清單 2 所示 volatile 狀態標誌麻煩很多。因 volatile 簡化編碼,且狀態標誌不依賴程式內任何其他狀態,此處非常適合用 volatile。
此類狀態標記一公共特性:通常只有一種狀態轉換;shutdownRequested 從 false 轉 true,然後程式停止。此模式可擴展來回轉換狀態標誌,但只有轉換週期不被察覺時才能擴展(false 到 true,再轉 false)。此外需某些原子狀態轉換機制,如原子變數。
缺乏同步導致無法實現可見性,使確定何時寫入物件參考而非基本值更難。缺乏同步情況下,可能遇到某物件參考更新值(由另一執行緒寫入)與該物件狀態舊值同時存在。(這就是造成著名雙重檢查鎖定問題根源,其中物件參考無同步下讀操作,產生問題:您可能看到更新參考,但仍透過該參考看到不完全構造物件)。
實現安全發布物件一技術是將物件參考定義為 volatile 型別。清單 3 展示範例,後台執行緒啟動階段從資料庫載入資料。其他代碼在能利用這些資料時,使用前檢查資料是否曾發布過。
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 參考發布值,供程式其他部分使用。
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 方法獨立屬性容器。基本原理:許多框架為易變資料持有者(如 HttpSession)提供容器,但放入容器中物件必須執行緒安全。
在 volatile bean 模式中,JavaBean 所有資料成員都是 volatile 型別,getter、setter 方法必須非常普通——除獲取或設置相應屬性外,不包含任何邏輯。此外,對物件參考資料成員,參考物件必須有效不可變。(這將禁止具有陣列值屬性,因陣列參考宣告為 volatile 時,只有參考而非陣列本身具有 volatile 語義)。對任何 volatile 變數,不變式或約束不能包含 JavaBean 屬性。清單 5 範例展示遵守 volatile bean 模式的 JavaBean:
@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 讀操作,通常優於無競爭鎖獲取開銷。
@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 變數獲得更佳效能。