單例模式簡介
想要唯一的創建一個對象,我們不通過約定,而是通過制定約束的方式去限制。雖然我們可以建立一個全局變量。
public singleton{
private Singleton() {}
}
通過構造方法私有化可以避免外部實例化,不過這樣我們也無法獲得實例,我們繼續改造代碼。
public Singleton{
private static Singleton singletonInstance;
private Singleton() {}
public static Singleton getInstance() {
if(singletonInstance == null) {
singletonInstance = new Singleton();
}
return singletonInstance;
}
}
通過靜態方法我們就可以通過類名去調用getInstance()來獲得實例啦,這樣比全局變量的好處就是延遲實例化,也叫懶漢模式,只有在用這個實例調用方法的時候,方法才被加入到內存中,當對象不用的時候,gc會將方法回收。既擁有全局變量的優點又避免了全局變量的缺點。
沒那么完美
看起來好像大功告成了,但仔細考慮,當多線程執行這段代碼的時候,可能會出現這樣的問題:
- 首先線程A首次訪問getInstance()判斷實例為null,線程A停止執行,線程B獲得時間片。
- 線程B首次訪問getInstance(),此刻線程A還未執行實例化語句,也判斷是null。
- 線程A、B都將創建一個Singleton。
分析原因
原因就在于getInstance方法并不是同步的,當線程A進入方法時,線程B也可以進入,于是我們可以給getInstance()方法上鎖,加上synchronized關鍵字,這樣問題就解決了。
public Singleton{
private static Singleton singletonInstance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(singletonInstance == null) {
singletonInstance = new Singleton();
}
return singletonInstance;
}
}
局部加鎖還是方法上鎖
好像已經很完美了吧,但是加了鎖之后效率非常低,我們觀察,其實只需要在第一次沒有創建對象的時候上鎖就可以了,當創建對象之后,不需要進行同步。那么我們只需要判斷當對象為空的時候,給創建對象代碼塊加鎖就可以了,當對象不為空的時候我們直接返回對象。
public Singleton{
private volatile static Singleton singletonInstance;
private Singleton() {}
public static Singleton getInstance() {
if(singletonInstance == null) {
synchronized (Singleton.class) {
if (singletonInstance == null) {
singletonInstance = new Singleton();
}
}
}
return singletonInstance;
}
}
a. 首先線程A首次訪問getInstance()判斷實例為null,假設線程A停止執行,線程B獲得時間片。
b. 線程B首次訪問getInstance(),此刻線程A還未執行實例化語句,也判斷是null。
c. 假設線程A獲得時間片,線程A獲得鎖,線程B無法進入,再次判斷是否為null,是則創建對象。線程A釋放鎖,線程A運行結束。
d. 線程B獲得時間片,線程B獲得鎖,再次判斷是否為null,結果為否,則不創建對象,線程B釋放鎖,線程B運行結束。
這種方式叫做double-checked locking(簡稱DCL,雙重檢查加鎖機制)。
volatile禁止重排序
由于JIT編譯器為了提高性能,可能在 singletonInstance = new Singleton();發生重排序,偽代碼為:
- memory = allocate(); //1:分配對象的內存空間
- ctorInstance(memory); //2:初始化對象
- instance = memory; //3:設置instance指向剛分配的內存地址
其中,2和3步驟可能會重排序,2和3得順序可能會跌倒。所以有這樣一種可能,當線程A實例對象時,執行順序為132,線程B進入代碼后看到instance得內存地址認為對象不為null,便返回實例,但是此刻線程A的初始化對象還沒有執行,所以返回得是空值。
解決方案
- 不允許2和3重排序;
- 允許2和3重排序,但不允許其他線程“看到”這個重排序。
當聲明對象的引用為volatile后,“問題的根源”的三行偽代碼中的2和3之間的重排序,在多線程環境中將會被禁止。
詳細請參考http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization
餓漢模式
除了DCL方法和方法上鎖以外,我們還可以在類加載器加載時在靜態初始化器直接創建,此方法也叫餓漢模式(對比懶漢是先甭管你需不需要直接創建)。(如果使用多個類加載器可能導致單件失效)
public Singleton{
private static Singleton singletonInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singletonInstance;
}
}
其他方案
- 使用static代碼塊實現單例模式
- 使用enum枚舉實現單例模式
總結
單例模式的主流方案有:
- 懶漢模式
- 餓漢模式
- DCL雙檢查機制
對于懶漢模式是非線程安全的,所以我們進行改進,先是通過同步方法的方式,然后因為效率的問題,我們采用了DCL雙檢查機制,其中還要考慮到此處的volatile關鍵字并非是保證線程可見性而是避免可重性的問題,另外,在序列化與反序列話的的過程中,要保證單例需要使用readResolve()方法進行保護。