把代碼塊聲明爲 synchronized,有兩個重要後果,一般是指該代碼具備 原子性(atomicity)和 可見性(visibility)。java
volatile的使用條件:編程
volatile變量具備 synchronized
的可見性特性,可是不具有原子性。這就是說線程可以自動發現 volatile 變量的最新值。 安全
volatile變量可用於提供線程安全,可是隻能應用於很是有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。所以,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具備與多個變量相關的不變式(Invariants)的類(例如 「start <=end」)。 多線程
出於簡易性或可伸縮性的考慮,您可能傾向於使用 volatile 變量而不是鎖。當使用 volatile 變量而非鎖時,某些習慣用法(idiom)更加易於編碼和閱讀。此外,volatile 變量不會像鎖那樣形成線程阻塞,所以也不多形成可伸縮性問題。在某些狀況下,若是讀操做遠遠大於寫操做,volatile 變量還能夠提供優於鎖的性能優點。 併發
使用條件框架
您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件: 函數
實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。 性能
第一個條件的限制使 volatile 變量不能用做線程安全計數器。雖然增量操做(x++
)看上去相似一個單獨操做,實際上它是一個由(讀取-修改-寫入)操做序列組成的組合操做,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操做須要使x
的值在操做期間保持不變,而 volatile 變量沒法實現這點。(然而,若是隻從單個線程寫入,那麼能夠忽略第一個條件。) 測試
反例this
大多數編程情形都會與這兩個條件的其中之一衝突,使得 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 變量的規範使用僅僅是使用一個布爾狀態標誌,用於指示發生了一個重要的一次性事件,例如完成初始化或請求停機。
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
)。此外,還須要某些原子狀態轉換機制,例如原子變量。
在缺少同步的狀況下,可能會遇到某個對象引用的更新值(由另外一個線程寫入)和該對象狀態的舊值同時存在。
這就是形成著名的雙重檢查鎖定(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; }
安全使用 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; } }
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; } }
若是讀操做遠遠超過寫操做,您能夠結合使用內部鎖和 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:
getInstance()
方法並決定 instance
在 //1 處爲null
。 if
代碼塊,但在執行 //2 處的代碼行時被線程 2 預佔。 getInstance()
方法並在 //1 處決定 instance
爲 null
。 if
代碼塊並建立一個新的 Singleton
對象並在 //2 處將變量instance
分配給這個新對象。 Singleton
對象引用。 Singleton
對象。 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;
}
假設有下列事件序列:
對於上面解說的賦值,卻沒有初始化的緣由,是因爲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.