在例1中的ClassicSingleton.getInstance()方法由於下面的代碼而不是安全執行緒的:
if(instance == null)
{
instance = new Singleton();
}
一種效能改進的方法
尋找一種效能改進方法時,你可能會選擇像下面這樣重寫getInstance()方法:
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
這個程式碼片段只同步了關鍵的代碼,而不是同步整個方法。然而這段代碼卻不是安全執行緒的。考慮一下下面的假定:線程1進入同步塊,並且在它給singleton成員變數賦值之前線程1被切換。接著另一個線程進入if塊。第二個線程將等待直到第一個線程完成,並且仍然會得到兩個不同的單例類執行個體。有修複這個問題的方法嗎?請讀下去。
雙重加鎖檢查
初看上去,雙重加鎖檢查似乎是一種使懶漢式執行個體化為安全執行緒的技術。下面的程式碼片段展示了這種技術:
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
如果兩個線程同時訪問getInstance()方法會發生什嗎?想像一下線程1進行同步塊馬上又被切換。接著,第二個線程進入if 塊。當線程1退出同步塊時,線程2會重新檢查看是否singleton執行個體仍然為null。因為線程1設定了singleton成員變數,所以線程2的第二次檢查會失敗,第二個單例類執行個體也就不會被建立。似乎就是如此。
不幸的是,雙重加鎖檢查不會保證正常工作,因為編譯器會在Singleton的構造方法被調用之前隨意給singleton賦一個值。如果在singleton引用被賦值之後而被初始化之前線程1被切換,線程2就會被返回一個對未初始化的單例類執行個體的引用。
一個改進的安全執行緒的單例模式實現
例7列出了一個簡單、快速而又是安全執行緒的單例模式實現:
例7.一個簡單的單例類
public class Singleton {
public final static Singleton INSTANCE = new Singleton();
private Singleton() { // Exists
}
}
這段代碼是安全執行緒的是因為靜態成員變數一定會在類被第一次訪問時被建立。你得到了一個自動使用了懶漢式執行個體化的安全執行緒的實現;你應該這樣使用它:
Singleton singleton = Singleton.INSTANCE; singleton.dothis(); singleton.dothat(); ...
當然萬事並不完美,前面的Singleton只是一個折衷的方案;如果你使用那個實現,你就無法改變它以便後來你可能想要允許多個單例類的執行個體。用一種更折哀的單例模式實現(通過一個getInstance()方法獲得執行個體)你可以改變這個方法以便返回一個唯一的執行個體或者是數百個執行個體中的一個.你不能用一個公開且是靜態(public static)成員變數這樣做.
你可以安全的使用例7的單例模式實現或者是例1的帶一個同步的getInstance()方法的實現.
---------------------------------------------------------------------------------------------------
實際上 看《Java設計模式 》一書8.2 單例和線程 你會發現那樣寫就不會有上面的問題了。
8.2 單例和線程
如果要在多線程環境中對單例採用滯後初始化,那麼我們必須小心防止多個線程同時初始化該單例。在多線程環境中,我們無法保證一個方法能夠持續運行到結束,其他線程的方法才開始運行。因而可能存在這樣一種情形:兩個線程幾乎同時嘗試初始化單例類。假設第一個方法發現單例為空白,而第二個方法在此刻開始運行,它也會發現該單例為空白。接下來,這兩個方法都將會對該單例類進行初始化。為了防止這種類型的競爭,需要提供一個鎖機制來協調不同線程中多個方法的運行。
Java語言及其類庫為多線程開發提供了很好的支援。特別地,Java為每個對象提供一個鎖(lock),它可以用於表示對象是否已被某個線程佔用。若要確保只有一個線程可以初始化單例類,我們可以利用適當的對象鎖來實現對單例初始化進行同步。其他方法,如需互斥訪問單例的方法,也可以基於鎖的方案進行同步。《Java並發編程》[Lea, 2000]一書建議使用屬於當前類的鎖進行同步。代碼如下:
getFactory()的代碼保證:在一個線程開始滯後初始化的時候,如果有另一個線程也準備開始初始化,這時候,第二個線程將停止執行,等待擷取對象classLock的鎖。當第二個線程擷取這個鎖並開始執行初始化的時候,它會發現該單例已不再為空白(因為只存在該類的唯一執行個體,我們可以使用單個靜態鎖)。
wipMoves變數記錄了半成品(WIP)已經完成的工序的數目。每當一個材料箱移到一台新機器上時,引發或記錄材料箱移動的子系統必須調用工廠單例的recordWipMove()方法。