前言
????單例模式應(yīng)該是編程中使用最多的設(shè)計模式之一,寫好單例模式往往能體現(xiàn)一個程序員的基本功。單例模式看似簡單,但是要將其設(shè)計得高效、安全、優(yōu)雅,還是需要考慮很多細(xì)節(jié)之處。本文將從最簡單的單例模式開始分析,從簡單-高效-安全-優(yōu)雅逐步演化。
非線程安全版
????簡單版只需實現(xiàn)單例功能,并不考慮其他因素。同時簡單版分餓漢和懶漢模式,至于何為餓漢何為懶漢,接下來將逐一介紹。
1. 餓漢模式
????何為餓漢模式,簡單形象來說就是像餓漢一樣,很饑餓,需要立馬得到食物。在代碼中體現(xiàn)就是,一開始就得把單例對象創(chuàng)建出來,需要時立馬就能拿到,不需要其他額外操作。代碼如下所示:
public class Singleton {
private Singleton() {}
// 餓漢模式
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
????從上面的代碼可以看出,餓漢模式相當(dāng)簡單,同時,這種方式也存在一些問題,一是靜態(tài)成員變量instance 在一開始就會被創(chuàng)建出來,即使整個運行階段都未使用,也會占用內(nèi)存;另一個是線程不安全,在多線程環(huán)境下會創(chuàng)建多個對象。針對這兩個問題,下面會逐一解決。
2. 懶漢模式
????和餓漢模式相反,懶漢模式如同其名,在對象需要使用時才創(chuàng)建出來,避免餓漢模式導(dǎo)致的內(nèi)存浪費。代碼如下所示:
public class Singleton {
private Singleton() {}
// 懶漢模式
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
????可以看到,懶漢模式也很簡單,instance 初始化為null,只有在需要使用時才去創(chuàng)建對象,解決了餓漢模式內(nèi)存浪費問題,但是線程安全問題依舊沒解決。下一小節(jié)將討論線程安全單例模式如何實現(xiàn)。
線程安全版
????在編寫多線程程序時,線程安全是最基本也是最重要的考慮之一。接下來,我們將討論線程安全版本單例模式的幾種實現(xiàn)方式。
1. 簡單粗暴法
????大家很容易想到,要想保證線程安全那直接在方法前加鎖使其成為同步方法,那么在多線程同時調(diào)用方法時,需要排隊執(zhí)行同步塊,保證了線程的安全。這種實現(xiàn)方式如下:
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
// 同步方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
????上訴方法在getInstance前加synchronized 關(guān)鍵字保證方法的同步調(diào)用,實現(xiàn)了線程安全版的單例模式,這種方法簡單而且也達(dá)到了目的,但是缺陷是效率極低,每次調(diào)用方法獲取單例對象時都得加鎖,如果調(diào)用頻繁則對程序性能影響很嚴(yán)重。針對效率問題,下面會給出解決方法。
2. Double CheckLock(DCL)法
????既然每次加鎖會導(dǎo)致效率問題,那我們可以考慮是否可以縮小鎖的粒度,在必須加鎖的情況下才加鎖,其他情況直接饒過加鎖操作。如下所示:
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) { // first check
synchronized (Singleton.class) {
if (instance == null) { // second check
instance = new Singleton();
}
}
}
return instance;
}
}
????大家注意到,只有當(dāng)單例對象為空時,才進(jìn)入同步代碼塊,不為空時,直接返回對象,這種方式對比上一種實現(xiàn)方式極大提高了程序的性能。同時,這里有兩次判空操作,有些同學(xué)可能疑惑為什么需要兩次,這里解釋下:第一次判空,相信大家都能理解,即當(dāng)對象為空時才進(jìn)入同步塊;第二次判空是為了避免這種情況---當(dāng)多個線程同時通過了第一次判空,進(jìn)入同步塊,如果沒有第二次判斷操作,則會創(chuàng)建出多個單例對象。這種方法解決了性能問題,但是依舊有不足之處,那內(nèi)存浪費和線程安全都解決了,那存在什么不足呢?這種方式真的就線程安全了嗎?no,這里還涉及到比較底層的細(xì)節(jié)---jvm編譯器指令重排。何為指令重排,相信大家都了解,如上面一行代碼instance = new Singleton();編譯成jvm指令可能如下:
address = allocate(); // 1. 給對象分配內(nèi)存空間
ctorInstance(address); // 2. 初始化對象
instance = address; // 3. 將instance指向?qū)ο?
如果編譯器不搞事,正常情況下,按上面的jvm指令執(zhí)行不會出現(xiàn)線程安全問題,但是如果jvm按照自己的優(yōu)化算法將上訴指令進(jìn)行優(yōu)化,就會出現(xiàn)下面這種情況:
address = allocate(); // 1. 給對象分配內(nèi)存空間
instance = address; // 2. 將instance指向?qū)ο?ctorInstance(address); // 3. 初始化對象
那如果按照上面的指令順序運行會出現(xiàn)什么問題呢,為了能夠形象的解釋這一問題,我把上訴執(zhí)行替換到j(luò)ava代碼中,如下:
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) { // first check
synchronized (Singleton.class) {
if (instance == null) { // second check
address = allocate(); // 1. 給對象分配內(nèi)存空間
instance = address; // 2. 將instance指向?qū)ο?(注意,執(zhí)行完這條命令后,instance將不為null)
ctorInstance(address); // 3. 初始化對象
}
}
}
return instance;
}
}
按照上訴代碼的執(zhí)行邏輯,假設(shè)有兩個線程a、b。當(dāng)a線程通過了兩次check,執(zhí)行完instance = address后,此時instance的狀態(tài)是不為null且未被初始化,假設(shè)此時b線程運行到第一次判空檢查發(fā)現(xiàn)instance已不為null,那直接返回instance,那么問題就來了,雖然此時instance不為null,但是其并未被初始化,如果直接拿來使用必然會導(dǎo)致程序出錯。因此出現(xiàn)線程安全問題。那么如何解決指令重排的問題呢?這里引入volatile關(guān)鍵字防止jvm指令重排,如下所示:
public class Singleton {
private Singleton() {}
private volatile static Singleton instance = null; // 防止指令重排
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3. 靜態(tài)內(nèi)部類實現(xiàn)模式
????前面的方法已經(jīng)解決了內(nèi)存浪費、效率及線程安全問題,那還有沒有更加優(yōu)雅的方法呢?答案是肯定的,這里引入靜態(tài)內(nèi)部類實現(xiàn)方法,即能解決上訴所有問題也能體現(xiàn)出代碼的優(yōu)雅感,如下:
public class Singleton {
private Singleton() {}
private static class Inner {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return Inner.instance;
}
}
????內(nèi)部類是延時加載的,也就是說只會在第一次使用時加載,不使用就不加載。使用內(nèi)部類可以在不加鎖的情況下,保證線程安全,因此用內(nèi)部類能夠優(yōu)雅的實現(xiàn)單例模式。
小結(jié)
????本文從內(nèi)存、性能、安全角度出發(fā),對單例模式的實現(xiàn)逐步演進(jìn),在保證這三者的前提下最終討論出一種優(yōu)雅的實現(xiàn)方法。如有不足之處,歡迎交流討論,謝謝。