前言
有一些對象其實我們隻需要一個,比方說:線程池,緩存,對話框,處理偏好設定和系統資料庫的對象,日志對象,充當列印機,顯示卡等裝置的驅動程式的對象。事實上,這類對象隻能有一個執行個體,如果制造出多個執行個體,就會導緻許多問題産生,例如:程式的行為異常,資源使用過量,或者是不一緻的結果
涉及到一些類加載的知識,如果不清楚,可以看一下這篇分享:Java類的加載順序
單例模式確定一個類隻有一個執行個體,并提供一個全局通路點,實作單例模式的方法是私有化構造函數,通過getInstance()方法執行個體化對象,并傳回這個執行個體
實作
第一種(懶漢)
按照上面的想法,我們有了第一個實作
// code1
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
複制
當2個線程同時進入getInstance()的if語句裡面,會傳回2個不同執行個體,是以這種方式是線程不安全的
// code2
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
複制
用synchronized修飾可以保證線程安全,但是隻有第一次執行此方法時才需要同步,設定好 uniqueInstance,就不需要同步這個方法了,之後每次調用這個方法,同步都是一種累贅
第二種(雙重檢查鎖定)
synchronized鎖的粒度太大,人們就想到通過雙重檢查鎖定來降低同步的開銷,下面是執行個體代碼
// code3
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
複制
如上面代碼所示,如果第一次檢查uniqueInstance不為null,那麼就不需要執行下面的加鎖和初始化操作,可以大幅降低synchronized帶來的性能開銷,隻有在多個線程試圖在同一時間建立對象時,會通過加鎖來保證隻有一個線程能建立對象
雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優化!線上程執行到getInstance()方法的第4行,代碼讀取到uniqueInstance不為null時,uniqueInstance引用的對象有可能還沒有完成初始化
簡單概述一下《Java并發程式設計的藝術》的解釋,
uniqueInstance = new Singleton()可以分解為如下三行僞代碼
memory = allocate(); // 1:配置設定對象的記憶體空間
ctorInstance(memory); // 2:初始化對象
uniqueInstance = memory;// 3:設定uniqueInstance指向剛配置設定的記憶體位址
複制
3行僞代碼中的2和3之間,可能會被重排序,重排序後執行時序如下
memory = allocate(); // 1:配置設定對象的記憶體空間
uniqueInstance = memory;// 3:設定uniqueInstance指向剛配置設定的記憶體位址
// 注意,此時對象還沒有被初始化
ctorInstance(memory); // 2:初始化對象
複制
多個線程通路時可能出現如下情況
時間
線程A
線程B
t1
A1:配置設定對象的記憶體空間
t2
A3:設定uniqueinstance指向記憶體空間
t3
B1:判斷uniqueinstance是否為空
t4
B2:由于uniqueinstace不為null,線程B間通路uniqueinstance引用的對象
t5
A2:初始化對象
t6
A4:通路instace引用的對象
這樣會導緻線程B通路到一個還未初始化的對象,此時可以用volatile來修飾Singleton,這樣3行僞代碼中的2和3之間的重排序,在多線程環境中将會被禁止
// code4
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
複制
第三種(餓漢)
如果應用程式總是建立并使用單例式例,或者在建立和運作時方面的負擔不太繁重,我們可以以餓漢式的方式來建立單例
// code5
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
}
複制
// code6
public class Singleton {
private static Singleton uniqueInstance;
static {
uniqueInstance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
}
複制
在類加載的時候直接建立這個對象,這樣既能提高效率,又能保證線程安全,code5和code6幾乎沒有差別,因為靜态成員變量和靜态代碼塊都是類初始化的時候被加載
第四種(靜态内部類)
// code7
public class Singleton {
private static class SingletonHolder {
private static Singleton uniqueInstance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.uniqueInstance;
}
}
複制
餓漢式的方式隻要Singleton類被裝載了,那麼uniqueInstance就會被執行個體化(沒有達到lazy loading效果),而這種方式是Singleton類被裝載了,uniqueInstance不一定被初始化。因為SingletonHolder類沒有被主動使用,隻有顯示通過調用getInstance方法時,才會顯示裝載SingletonHolder類,進而執行個體化uniqueInstance
第五種(枚舉)
// code8
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
複制
公認的實作單例的最好方式,網上資料也比較少,還沒有徹底了解清楚,說不定以後會補一篇文章說明,本篇文章中的code1和code3不建議使用,原因已經說明
參考書籍
《Java并發程式設計的藝術》
《Head First設計模式》