對幾種單例寫法的整理,并分析其優(yōu)缺點。如何創(chuàng)建一個線程安全的單例,什么是雙檢鎖,那這篇文章可能會幫助到你。
懶漢式 非線程安全
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
代碼簡單明了,使用了懶加載模式,但是卻存在問題。當有多個線程并行調(diào)用 getInstance() 的時,就會創(chuàng)建多個實例。在多線程下不能正常工作。
懶漢式,線程安全
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
雖做到線程安全,并且解決了多實例的問題,但是并不高效。因為在任何時候只能有一個線程調(diào)用 getInstance() 方法。但是同步操作只在第一次調(diào)用時才被需要,即第一次創(chuàng)建單例實例時。雙重檢驗鎖就能解決這個問題。
雙重檢驗鎖
雙重檢驗鎖,是一種使用同步塊加鎖的方法。又稱其為雙重檢查鎖,因為會有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內(nèi)。為何在同步塊內(nèi)還要再檢驗?因為可能會有多個線程一起進入同步塊外的 if,如果在同步塊內(nèi)不進行二次檢驗的話就會生成多個實例。
public class Singleton {
private volatile static Singleton instance;//聲明volatile,原子操作
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) { //Single Checked
synchronized (Singleton.class) {
if (instance == null) { //Double Checked
instance = new Singleton();
}
}
}
return instance;
}
}
- 給 instance 分配內(nèi)存
- 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
- 將instance對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)
在 JVM 的即時編譯器中存在指令重排序的優(yōu)化。上面的2和3的順序是不能保證的,最終執(zhí)行順序有可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被其他線程搶占,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,最后報錯,所以才要使用volatile
volatile
- 禁止指令重排序優(yōu)化。也就是說,在 volatile 變量的賦值操作后面會有一個內(nèi)存屏障(生成的匯編代碼上),讀操作不會被重排序到內(nèi)存屏障之前。比如上面的例子,取操作必須在執(zhí)行完 1-2-3 之后或者 1-3-2 之后,不存在執(zhí)行到 1-3 然后取到值的情況。從「先行發(fā)生原則」的角度理解的話,就是對于一個 volatile 變量的寫操作都先行發(fā)生于后面對這個變量的讀操作(這里的“后面”是時間上的先后順序)。
- 特別注意在 Java 5 以前的版本使用 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 內(nèi)存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前后的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,所以在這之后才可以放心使用 volatile。
餓漢式 static final field
單例的實例被聲明成 static 和 final 變量,開始就加載類到內(nèi)存中時就會初始化,所以創(chuàng)建實例本身是線程安全的。
public class Singleton{
//類加載時就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
- 缺點:不是一種懶加載模式
- 單例會在加載類后一開始就被初始化,即使客戶端沒有調(diào)用 getInstance()方法。
- 餓漢式的創(chuàng)建方式在某些場景中無法使用:如 Singleton 實例的創(chuàng)建是依賴參數(shù)或配置文件的,在 getInstance() 之前必須調(diào)用某個方法設置參數(shù)給它,這種單例就無法使用了。
靜態(tài)內(nèi)部類 static nested class
該方法也是《Effective Java》上推薦的。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 使用JVM本身機制保證了線程安全問題;
- 由于
SingletonHolder是私有的,除了 getInstance() 外沒有辦法訪問它,懶漢式; - 同時讀取實例的時候不會進行同步,沒有性能缺陷;
- 不依賴 JDK 版本。
枚舉 Enum
用枚舉寫單例太簡單
public enum Singleton {
INSTANCE;
}
可通過Singleton.INSTANCE來訪問實例,這比調(diào)用getInstance()方法簡單多了。創(chuàng)建枚舉默認就是線程安全的,還能防止反序列化導致重新創(chuàng)建新的對象。但還是很少看到有人這樣寫。
總結(jié)
一般來說,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態(tài)內(nèi)部類、枚舉。上述所說都是線程安全的實現(xiàn)
- 一般使用餓漢式
- 要求懶加載傾向靜態(tài)內(nèi)部類
- 反序列化創(chuàng)建對象用枚舉。