Java 語言中的 volatile 變量能夠被看做是一種 「程度較輕的 synchronized」;與synchronized 塊相比,volatile 變量所需的編碼較少,而且運行時開銷也較少,可是它所能實現的功能也僅是 synchronized 的一部分。本文介紹了幾種有效使用 volatile 變量的模式,並強調了幾種不適合使用 volatile 變量的情形。
鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互斥即一次只容許一個線程持有某個特定的鎖,所以可以使用該特性實現對共享數據的協調訪問協議,這樣,一次就只有一個線程可以使用該共享數據。可見性要更加複雜一些,它必須確保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的 —— 若是沒有同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這將引起許多嚴重問題。
與鎖相比,Volatile 變量是一種很是簡單但同時又很是脆弱的同步機制,它在某些狀況下將提供優於鎖的性能和伸縮性。若是嚴格遵循 volatile 的使用條件 —— 即變量真正獨立於其餘變量和本身之前的值 —— 在某些狀況下可使用volatile 代替 synchronized 來簡化代碼。然而,使用 volatile 的代碼每每比使用鎖的代碼更加容易出錯。
爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。若是對聲明volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題。因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。
下面來具體講解volatile的兩條實現原則。
1)Lock前綴指令會引發處理器緩存回寫到內存
2)一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效
鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。
Volatile 變量
Volatile 變量具備 synchronized 的可見性特性,可是不具有原子特性。這就是說線程可以自動發現 volatile 變量的最新值。Volatile 變量可用於提供線程安全,可是隻能應用於很是有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。所以,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具備與多個變量相關的不變式(Invariants)的類(例如 「start <=end」)。
出於簡易性或可伸縮性的考慮,您可能傾向於使用 volatile 變量而不是鎖。當使用 volatile 變量而非鎖時,某些習慣用法(idiom)更加易於編碼和閱讀。此外,volatile 變量不會像鎖那樣形成線程阻塞,所以也不多形成可伸縮性問題。在某些狀況下,若是讀操做遠遠大於寫操做,volatile 變量還能夠提供優於鎖的性能優點。
正確使用 volatile 變量的條件
您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:
- 對變量的寫操做不依賴於當前值。
- 該變量沒有包含在具備其餘變量的不變式中。
實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。
第一個條件的限制使 volatile 變量不能用做線程安全計數器。雖然增量操做(x++)看上去相似一個單獨操做,實際上它是一個由讀取-修改-寫入操做序列組成的組合操做,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操做須要使x 的值在操做期間保持不變,而 volatile 變量沒法實現這點。(然而,若是將值調整爲只從單個線程寫入,那麼能夠忽略第一個條件。)
大多數編程情形都會與這兩個條件的其中之一衝突,使得 volatile 變量不能像 synchronized 那樣廣泛適用於實現線程安全。
性能考慮
使用 volatile 變量的主要緣由是其簡易性:在某些情形下,使用 volatile 變量要比使用相應的鎖簡單得多。使用 volatile 變量次要緣由是其性能:某些狀況下,volatile 變量同步機制的性能要優於鎖。
volatile 操做不會像鎖同樣形成阻塞,所以,在可以安全使用 volatile 的狀況下,volatile 能夠提供一些優於鎖的可伸縮特性。若是讀操做的次數要遠遠超過寫操做,與鎖相比,volatile 變量一般可以減小同步的性能開銷。
正確使用 volatile 的模式
不少併發性專家事實上每每引導用戶遠離 volatile 變量,由於使用它們要比使用鎖更加容易出錯。然而,若是謹慎地遵循一些良好定義的模式,就可以在不少場合內安全地使用 volatile 變量。要始終牢記使用 volatile 的限制 —— 只有在狀態真正獨立於程序內其餘內容時才能使用 volatile —— 這條規則可以避免將這些模式擴展到不安全的用例。
模式 #1:狀態標誌
也許實現 volatile 變量的規範使用僅僅是使用一個布爾狀態標誌,用於指示發生了一個重要的一次性事件,例如完成初始化或請求停機。
不少應用程序包含了一種控制結構,形式爲 「在尚未準備好中止程序時再執行一些工做」,如清單 2 所示:
清單 2. 將 volatile 變量做爲狀態標誌使用
private 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)。此外,還須要某些原子狀態轉換機制,例如原子變量。
清單 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 類型的引用能夠確保對象的發佈形式的可見性,可是若是對象的狀態在發佈後將發生更改,那麼就須要額外的同步。
模式 #3:獨立觀察(independent observation)
安全使用 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;
}
}
該模式是前面模式的擴展;將某個值發佈以在程序內的其餘地方使用,可是與一次性事件的發佈不一樣,這是一系列獨立事件。這個模式要求被髮布的值是有效不可變的 —— 即值的狀態在發佈後不會更改。使用該值的代碼須要清楚該值可能隨時發生變化。
模式 #4:「volatile bean」 模式
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 用例的緣由是它可以提高性能,確保在開始應用高級模式以前,真正肯定須要實現這種性能獲益。須要對這些模式進行權衡,放棄可讀性或可維護性來換取可能的性能收益 —— 若是您不須要提高性能(或者不可以經過一個嚴格的測試程序證實您須要它),那麼這極可能是一次糟糕的交易,由於您極可能會得不償失,換來的東西要比放棄的東西價值更低。
模式 #5:開銷較低的讀-寫鎖策略
目前爲止,您應該瞭解了 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 保證讀代碼路徑時,要比使用鎖執行所有代碼路徑得到更高的共享度 —— 就像讀-寫操做同樣。然而,要隨時牢記這種模式的弱點:若是超越了該模式的最基本應用,結合這兩個競爭的同步機制將變得很是困難。 緩存