volatile的適用場景
介紹
把代碼塊聲明為 synchronized,有兩個重要後果,通常是指該代碼具有 原子性(atomicity)和 可見性(visibility)。
- 原子性意味著個時刻,只有一個線程能夠執行一段代碼,這段代碼通過一個monitor object保護。從而防止多個線程在更新共享狀態時相互沖突。 所謂原子性操作是指不會被線程調度機子打斷的操作,這種操作一旦開始,就一直到幸運星結束,中間不會有任何切換(切換線程)。
- 可見性則更為微妙,它必須確保釋放鎖之前對共享數據做出的更改對於隨後獲得該鎖的另一個線程是可見的。 —— 如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。
volatile的使用條件:
volatile變量具有 synchronized
的可見性特性,但是不具備原子性。這就是說線程能夠自動發現 volatile 變量的最新值。
volatile變量可用於提供線程安全,但是只能應用於非常有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。因此,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具有與多個變量相關的不變式(Invariants)的類(例如 “start <=end”)。
出於簡易性或可伸縮性的考慮,您可能傾向於使用 volatile 變量而不是鎖。當使用 volatile 變量而非鎖時,某些習慣用法(idiom)更加易於編碼和閱讀。此外,volatile 變量不會像鎖那樣造成線程阻塞,因此也很少造成可伸縮性問題。在某些情況下,如果讀操作遠遠大於寫操作,volatile 變量還可以提供優於鎖的性能
使用條件
您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:
- 對變量的寫操作不依賴於當前值。
- 該變量沒有包含在具有其他變量的不變式中。
實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。
第一個條件的限制使 volatile 變量不能用作線程安全計數器。雖然增量操作(x++
)看上去類似一個單獨操作,實際上它是一個由(讀取-修改-寫入)操作序列組成的組合操作,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操作需要使x
反例
大多數編程情形都會與這兩個條件的其中之一沖突,使得 volatile 變量不能像 synchronized
那樣普遍適用於實現線程安全。
【反例:volatile變量不能用於約束條件中】 下面是一個非線程安全的數值範圍類。它包含了一個不變式 —— 下界總是小於或等於上界。
public class NumberRange { private volatile int lower; private volatile int 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()
操作原子化。
否則,如果湊巧兩個線程在同一時間使用不一致的值執行 setLower
和 setUpper
的話,則會使範圍處於不一致的狀態。例如,如果初始狀態是(0, 5)
,同一時間內,線程 A 調用setLower(4)
並且線程 B 調用setUpper(3)
,顯然這兩個操作交叉存入的值是不符合條件的,那麽兩個線程都會通過用於保護不變式的檢查,使得最後的範圍值是(4, 3)
—— 一個無效值。
volatile的適用場景
模式 #1:狀態標誌
也許實現 volatile 變量的規範使用僅僅是使用一個布爾狀態標誌,用於指示發生了一個重要的一次性事件,例如完成初始化或請求停機。
volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } }
線程1執行doWork()的過程中,可能有另外的線程2調用了shutdown,所以boolean變量必須是volatile。
而如果使用 synchronized
塊編寫循環要比使用 volatile 狀態標誌編寫麻煩很多。由於 volatile 簡化了編碼,並且狀態標誌並不依賴於程序內任何其他狀態,因此此處非常適合使用 volatile。
這種類型的狀態標記的一個公共特性是:通常只有一種狀態轉換;shutdownRequested
標誌從false
轉換為true
,然後程序停止。這種模式可以擴展到來回轉換的狀態標誌,但是只有在轉換周期不被察覺的情況下才能擴展(從false
到true
,再轉換到false
)。此外,還需要某些原子狀態轉換機制,例如原子變量。
模式 #2:一次性安全發布(one-time safe publication)
在缺乏同步的情況下,可能會遇到某個對象引用的更新值(由另一個線程寫入)和該對象狀態的舊值同時存在。
這就是造成著名的雙重檢查鎖定(double-checked-locking)問題的根源,其中對象引用在沒有同步的情況下進行讀操作,產生的問題是您可能會看到一個更新的引用,但是仍然會通過該引用看到不完全構造的對象。如下面介紹的單例模式。
private volatile static Singleton instace; public static Singleton getInstance(){ //第一次null檢查 if(instance == null){ synchronized(Singleton.class) { //1 //第二次null檢查 if(instance == null){ //2 instance = new Singleton();//3 } } } return instance; }
模式 #3:獨立觀察(independent observation)
安全使用 volatile 的另一種簡單模式是:定期 “發布” 觀察結果供程序內部使用。【例如】假設有一種環境傳感器能夠感覺環境溫度。一個後臺線程可能會每隔幾秒讀取一次該傳感器,並更新包含當前文檔的 volatile 變量。然後,其他線程可以讀取這個變量,從而隨時能夠看到最新的溫度值。
使用該模式的另一種應用程序就是收集程序的統計信息。
【例】如下代碼展示了身份驗證機制如何記憶最近一次登錄的用戶的名字。將反復使用lastUser
引用來發布值,以供程序的其他部分使用。(主要利用了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 模式的基本原理是:很多框架為易變數據的持有者(例如 HttpSession
)提供了容器,但是放入這些容器中的對象必須是線程安全的。
在 volatile bean 模式中,JavaBean 的所有數據成員都是 volatile 類型的,並且 getter 和 setter 方法必須非常普通——即不包含約束!
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; } }
模式 #5:開銷較低的“讀-寫鎖”策略
如果讀操作遠遠超過寫操作,您可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。
如下顯示的線程安全的計數器,使用 synchronized
確保增量操作是原子的,並使用 volatile
保證當前結果的可見性。如果更新不頻繁的話,該方法可實現更好的性能,因為讀路徑的開銷僅僅涉及 volatile 讀操作,這通常要優於一個無競爭的鎖獲取的開銷。
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; //讀操作,沒有synchronized,提高性能 public int getValue() { return value; } //寫操作,必須synchronized。因為x++不是原子操作 public synchronized int increment() { return value++; } }
使用鎖進行所有變化的操作,使用 volatile 進行只讀操作。
其中,鎖一次只允許一個線程訪問值,volatile 允許多個線程執行讀操作。
單例模式
定義:
確保某個類只有一個實例,並提供一個全局訪問點。
類圖:
public class Singleton{ private static final Singleton instance; private Singleton(){ } public static Singleton getInstance(){ if(instance == null){ //1 instance = new Singleton();//2 } return instance; //3 } ... }
優點:
- 內存中只有一個對象,減少內存開支;
- 單例可避免對資源的多重占用,例如寫文件動作,可避免對同一資源文件的同時寫操作。
缺點:
- 單例模式一般沒有接口,擴展很困難; ——單例並不是用來繼承的。
- 不利於測試,並行開發時,若單例未完成,則不能進行測試;
- 與單一職責原則沖突,把“要單例”和業務邏輯融合在一個類中。
使用場景:
若出現多個對象就會出現“不良反應”,應該用單例,具體場景如下:
- 要求生成唯一序列號的環境;
- 在整個項目中需要一個共享訪問點或共享數據。例如頁面計數器;
- 創建一個對象需要消耗的資源過多時;
- 需要定義大量的靜態常量和靜態方法的環境。
為什麽不直接用全局變量來實現單例?
有缺點:全局變量必須在程序一開始就創建好。而單例模式可以延遲初始化。
類加載器對單例的影響:
不同的類加載器可能會加載同一個類。
如果程序有多個類加載器,可在單例中指定某個加載器,並指定同一個加載器。
多線程的影響:
上文代碼示例在多線程環境下有bug:
- 線程 1 調用
getInstance()
方法並決定instance
在 //1 處為null
。 - 線程 1 進入
if
代碼塊,但在執行 //2 處的代碼行時被線程 2 預占。 - 線程 2 調用
getInstance()
方法並在 //1 處決定instance
為null
。 - 線程 2 進入
if
代碼塊並創建一個新的Singleton
對象並在 //2 處將變量instance
分配給這個新對象。 - 線程 2 在 //3 處返回
Singleton
對象引用。 - 線程 2 被線程 1 預占。
- 線程 1 在它停止的地方啟動,並執行 //2 代碼行,這導致創建另一個
Singleton
對象。 - 線程 1 在 //3 處返回這個對象。
getInstance()
方法創建了兩個 Singleton
對象。
解決方法一:不用延遲初始化
public class Singleton{ private static final Singleton instance = new Singleton(); private Singleton(){ } public static Singleton getInstance(){ return instance; } ... }
解決方法二:同步getInstance
public class Singleton{ private static final Singleton instance; private Singleton(){ } //同步getInstance public static synchronized Singleton getInstance(){ if(instance == null){ //1 instance = new Singleton();//2 } return instance; //3 } ... }
但是synchronized
方法會降低性能,尤其這裏僅當第一次調用getInstance時才需要同步,只有執行//2代碼行時才需要同步。
你可能想到只同步方法塊,即只對//2進行同步:
public static Singleton getInstance(){ if(instance == null){ synchronized(Singleton.class) { instance = new Singleton(); } } return instance; }
但這樣做並不能解決問題:
當 instance 為 null 時,兩個線程可以並發地進入if 語句內部。
然後,一個線程進入 synchronized 塊來初始化 instance,而另一個線程則被阻斷。
當第一個線程退出 synchronized 塊時,等待著的線程進入並創建另一個Singleton 對象。
註意:當第二個線程進入 synchronized 塊時,它並沒有檢查 instance 是否非 null。
還是會創建2個對象。
解決方法三:雙重檢查加鎖
針對上述方法的缺點,我們在//2代碼行時 再檢查一次null,就能保證只創建一個對象:
//註意volatile!! private volatile static Singleton instace; public static Singleton getInstance(){ //第一次null檢查 if(instance == null){ synchronized(Singleton.class) { //1 //第二次null檢查 if(instance == null){ //2 instance = new Singleton();//3 } } } return instance;
}
假設有下列事件序列:
- 線程 1 進入 getInstance() 方法。
- 由於 instance 為 null,線程 1 在 //1 處進入synchronized 塊。
- 線程 1 被線程 2 預占。
- 線程 2 進入 getInstance() 方法。
- 由於 instance 仍舊為 null,線程 2 試圖獲取 //1 處的鎖。然而,由於線程 1 持有該鎖,線程 2 在 //1 處阻塞。
- 線程 2 被線程 1 預占。
- 線程 1 執行,由於在 //2 處實例仍舊為 null,線程 1 還創建一個Singleton 對象並將其引用賦值給instance(由於java執行的無序性,可能賦值時只是占用內存空間(此時instance已經為非null,鎖松開,由於無序性,還沒有來得及初始化,線程2已經取得instance對象),還沒有根據構造函數初始化)。
- 線程 1 退出 synchronized 塊並從 getInstance() 方法返回實例。
- 線程 1 被線程 2 預占。
- 線程 2 獲取 //1 處的鎖並檢查 instance 是否為 null。
- 由於 instance 是非 null 的,並沒有創建第二個Singleton 對象,由線程 1 創建的對象被返回,此時返回對象可能是是一個構造完整卻沒有完全初始化的對象。
- 線程1繼續執行完成對象的初始化,由於instance是volatile類型的,所以instance變量對所有線程共享可見,所以線程2可以得到一個完整初始化的對象。
對於上面解說的賦值,卻沒有初始化的原因,是由於java變量重新賦值時有3個步驟的(讀取,修改,回寫)
代碼行 instance =new Singleton();
執行了下列偽代碼
1. mem = allocate(); //Allocate memory for Singleton object. 2. instance = mem; //Note that instance is now non-null, but //has not been initialized. 3. ctorSingleton(instance); //Invoke constructor for Singleton passing //instance.
volatile的適用場景