Java中的單例模式
單例模式是 Java 中最簡單的設(shè)計模式之一,它是指一個類在運行期間始終只有一個實例,我們就把它稱之為單例模式。它不但被應(yīng)用在實際的工作中,而且還是面試中最??嫉念}目之一。通過單例模式我們可以知道此人的編程風(fēng)格,以及對于基礎(chǔ)知識的掌握是否牢固。
如何實現(xiàn)單例
構(gòu)造函數(shù)需要是 private 訪問權(quán)限的,這樣才能避免外部通過 new 創(chuàng)建實例
考慮對象創(chuàng)建時的線程安全問題
考慮是否支持延遲加載
考慮 getInstance() 性能是否高(是否加鎖)
我們本課時的面試題是,單例的實現(xiàn)方式有幾種?它們有什么優(yōu)缺點?
典型回答
單例的實現(xiàn)分為餓漢模式和懶漢模式。顧名思義,餓漢模式就好比他是一個餓漢,而且有一定的危機(jī)意識,他會提前把食物囤積好,以備餓了之后直接能吃到食物。對應(yīng)到程序中指的是,在類加載時就會進(jìn)行單例的初始化,以后訪問時直接使用單例對象即可。
餓漢模式的實現(xiàn)代碼如下:
public?class?Singleton?{
????//?聲明私有對象
????private?static?Singleton?instance?=?new?Singleton();????
????//?獲取實例(單例對象)
????public?static?Singleton?getInstance()?{
????????return?instance;
????}
????private?Singleton()?{
????}
????//?方法
????public?void?sayHi()?{
????????System.out.println("Hi,Java.");
????}
}
class?SingletonTest?{
????public?static?void?main(String[]?args)?{
????????//?調(diào)用單例對象
????????Singleton?singleton?=?Singleton.getInstance();
????????//?調(diào)用方法
????????singleton.sayHi();
????}
}
以上程序的執(zhí)行結(jié)果為:
Hi,Java.
從上述結(jié)果可以看出,單例對象已經(jīng)被成功獲取到并順利地執(zhí)行了類中的方法。它的優(yōu)點是線程安全,因為單例對象在類加載的時候就已經(jīng)被初始化了,當(dāng)調(diào)用單例對象時只是把早已經(jīng)創(chuàng)建好的對象賦值給變量;它的缺點是可能會造成資源浪費,如果類加載了單例對象(對象被創(chuàng)建了),但是一直沒有使用,這樣就造成了資源的浪費。
懶漢模式也被稱作為飽漢模式,顧名思義他比較懶,每次只有需要吃飯的時候,才出去找飯吃,而不是像餓漢那樣早早把飯準(zhǔn)備好。對應(yīng)到程序中指的是,當(dāng)每次需要使用實例時,再去創(chuàng)建獲取實例,而不是在類加載時就將實例創(chuàng)建好。
懶漢模式的實現(xiàn)代碼如下:
public?class?Singleton?{
????//?聲明私有對象
????private?static?Singleton?instance;
????//?獲取實例(單例對象)
????public?static?Singleton?getInstance()?{
????????if?(instance?==?null)?{
????????????instance?=?new?Singleton();
????????}
????????return?instance;
????}
????private?Singleton()?{
????}
????//?方法
????public?void?sayHi()?{
????????System.out.println("Hi,Java.");
????}
}
class?SingletonTest?{
????public?static?void?main(String[]?args)?{
????????Singleton?singleton?=?Singleton.getInstance();
????????singleton.sayHi();
????}
}
以上程序的執(zhí)行結(jié)果為:
Hi,Java.
從上述結(jié)果可以看出,單例對象已經(jīng)被成功獲取到并順利地執(zhí)行了類中的方法,它的優(yōu)點是不會造成資源的浪費,因為在調(diào)用的時候才會創(chuàng)建被實例化對象;它的缺點在多線程環(huán)境下是非線程是安全的,比如多個線程同時執(zhí)行到 if 判斷處,此時判斷結(jié)果都是未被初始化,那么這些線程就會同時創(chuàng)建 n 個實例,這樣就會導(dǎo)致意外的情況發(fā)生。
對比分析
使用單例模式可以減少系統(tǒng)的內(nèi)存開銷,提高程序的運行效率,但是使用不當(dāng)?shù)脑捑蜁斐啥嗑€程下的并發(fā)問題。餓漢模式為最直接的實現(xiàn)單例模式的方法,但它可能會造成對系統(tǒng)資源的浪費,所以只有既能保證線程安全,又可以避免系統(tǒng)資源被浪費的回答才能徹底地征服面試官。
和此知識點相關(guān)的面試題還有以下這些:
什么是雙重檢測鎖?它是線程安全的嗎?
單例的還有其他實現(xiàn)方式嗎?
雙重校驗鎖
為了保證懶漢模式的線程安全我們最簡單的做法就是給獲取實例的方法上加上 synchronized(同步鎖)修飾,如下代碼所示:
public?class?Singleton?{
????//?聲明私有對象
????private?static?Singleton?instance;
????//?獲取實例(單例對象)
????public?synchronized?static?Singleton?getInstance()?{
????????if?(instance?==?null)?{
????????????instance?=?new?Singleton();
????????}
????????return?instance;
????}
????private?Singleton()?{
????}
????//?類方法
????public?void?sayHi()?{
????????System.out.println("Hi,Java.");
????}
}
這樣雖然能讓懶漢模式變成線程安全的,但由于整個方法都被 synchronized 所包圍,因此增加了同步開銷,降低了程序的執(zhí)行效率。
于是為了改進(jìn)程序的執(zhí)行效率,我們將 synchronized 放入到方法中,以此來減少被同步鎖所修飾的代碼范圍,實現(xiàn)代碼如下:
public?class?Singleton?{
????//?聲明私有對象
????private?static?Singleton?instance;
????//?獲取實例(單例對象)
????public?static?Singleton?getInstance()?{
????????if?(instance?==?null)?{
????????????synchronized?(Singleton.class)?{
????????????????instance?=?new?Singleton();
????????????}
????????}
????????return?instance;
????}
????private?Singleton()?{
????}
????//?類方法
????public?void?sayHi()?{
????????System.out.println("Hi,Java.");
????}
}
細(xì)心的你可能會發(fā)現(xiàn)以上的代碼也存在著非線程安全的問題。例如,當(dāng)兩個線程同時執(zhí)行到「if (instance == null) { 」判斷時,判斷的結(jié)果都為 true,于是他們就排隊都創(chuàng)建了新的對象,這顯然不符合我們的預(yù)期。于是就誕生了大名鼎鼎的雙重檢測鎖(Double Checked Lock,DCL),實現(xiàn)代碼如下:
public?class?Singleton?{
????//?聲明私有對象
????private?static?Singleton?instance;
????//?獲取實例(單例對象)
????public?static?Singleton?getInstance()?{
????????//?第一次判斷
????????if?(instance?==?null)?{
????????????synchronized?(Singleton.class)?{
????????????????//?第二次判斷
????????????????if?(instance?==?null)?{
????????????????????instance?=?new?Singleton();
????????????????}
????????????}
????????}
????????return?instance;
????}
????private?Singleton()?{
????}
????//?類方法
????public?void?sayHi()?{
????????System.out.println("Hi,Java.");
????}
}
上述代碼看似完美,其實隱藏著一個不容易被人發(fā)現(xiàn)的小問題,該問題就出在 new 對象這行代碼上,也就是 instance = new Singleton() 這行代碼。這行代碼看似是一個原子操作,然而并不是,這行代碼最終會被編譯成多條匯編指令,它大致的執(zhí)行流程為以下三個步驟:
給對象實例分配內(nèi)存空間;
調(diào)用對象的構(gòu)造方法、初始化成員字段;
將 instance 對象指向分配的內(nèi)存空間。
但由于 CPU 的優(yōu)化會對執(zhí)行指令進(jìn)行重排序,也就說上面的執(zhí)行流程的執(zhí)行順序有可能是 1-2-3,也有可能是 1-3-2。假如執(zhí)行的順序是 1-3-2,那么當(dāng) A 線程執(zhí)行到步驟 3 時,切換至 B 線程了,而此時 B 線程判斷 instance 對象已經(jīng)指向了對應(yīng)的內(nèi)存空間,并非為 null 時就會直接進(jìn)行返回,而此時因為沒有執(zhí)行步驟 2,因此得到的是一個未初始化完成的對象,這樣就導(dǎo)致了問題的誕生。執(zhí)行時間節(jié)點如下表所示:
| 時間點 | 線程 | 執(zhí)行操作 |
|---|---|---|
| t1 | A | instance = new Singleton() 的 1-3 步驟,待執(zhí)行步驟 2 |
| t2 | B | if (instance == null) {判斷結(jié)果為 false |
| t3 | B | 返回半初始的 instance 對象 |
為了解決此問題,我們可以使用關(guān)鍵字 volatile 來修飾 instance 對象,這樣就可以防止 CPU 指令重排,從而完美地運行懶漢模式,實現(xiàn)代碼如下:
public?class?Singleton?{
????//?聲明私有對象
????private?volatile?static?Singleton?instance;
????//?獲取實例(單例對象)
????public?static?Singleton?getInstance()?{
????????//?第一次判斷
????????if?(instance?==?null)?{
????????????synchronized?(Singleton.class)?{
????????????????//?第二次判斷
????????????????if?(instance?==?null)?{
????????????????????instance?=?new?Singleton();
????????????????}
????????????}
????????}
????????return?instance;
????}
????private?Singleton()?{
????}
????//?類方法
????public?void?sayHi()?{
????????System.out.println("Hi,Java.");
????}
}
單例其他實現(xiàn)方式
除了以上的 6 種方式可以實現(xiàn)單例模式外,還可以使用靜態(tài)內(nèi)部類和枚舉類來實現(xiàn)單例。靜態(tài)內(nèi)部類的實現(xiàn)代碼如下:
public?class?Singleton?{
????//?靜態(tài)內(nèi)部類
????private?static?class?SingletonInstance?{
????????private?static?final?Singleton?instance?=?new?Singleton();
????}
????//?獲取實例(單例對象)
????public?static?Singleton?getInstance()?{
????????return?SingletonInstance.instance;
????}
????private?Singleton()?{
????}
????//?類方法
????public?void?sayHi()?{
????????System.out.println("Hi,Java.");
????}
}
從上述代碼可以看出,靜態(tài)內(nèi)部類和餓漢方式有異曲同工之妙,它們都采用了類裝載的機(jī)制來保證,當(dāng)初始化實例時只有一個線程執(zhí)行,從而保證了多線程下的安全操作。JVM 會在類初始化階段(也就是類裝載階段)創(chuàng)建一個鎖,該鎖可以保證多個線程同步執(zhí)行類初始化的工作,因此在多線程環(huán)境下,類加載機(jī)制依然是線程安全的。
但靜態(tài)內(nèi)部類和餓漢方式也有著細(xì)微的差別,餓漢方式是在程序啟動時就會進(jìn)行加載,因此可能造成資源的浪費;而靜態(tài)內(nèi)部類只有在調(diào)用 getInstance() 方法時,才會裝載內(nèi)部類從而完成實例的初始化工作,因此不會造成資源浪費的問題。由此可知,此方式也是較為推薦的單例實現(xiàn)方式。
單例的另一種實現(xiàn)方式為枚舉,它也是《Effective Java》作者極力推薦地單例實現(xiàn)方式,因為枚舉的實現(xiàn)方式不僅是線程安全的,而且只會裝載一次,無論是序列化、反序列化、反射還是克隆都不會新創(chuàng)建對象。它的實現(xiàn)代碼如下:
public?class?Singleton?{
????//?枚舉類型是線程安全的,并且只會裝載一次
????private?enum?SingletonEnum?{
????????INSTANCE;
????????//?聲明單例對象
????????private?final?Singleton?instance;
????????//?實例化
????????SingletonEnum()?{
????????????instance?=?new?Singleton();
????????}
????????private?Singleton?getInstance()?{
????????????return?instance;
????????}
????}
????//?獲取實例(單例對象)
????public?static?Singleton?getInstance()?{
????????return?SingletonEnum.INSTANCE.getInstance();
????}
????private?Singleton()?{
????}
????//?類方法
????public?void?sayHi()?{
????????System.out.println("Hi,Java.");
????}
}
class?SingletonTest?{
????public?static?void?main(String[]?args)?{
????????Singleton?singleton?=?Singleton.getInstance();
????????singleton.sayHi();
????}
}
以上程序的執(zhí)行結(jié)果為:
Hi,Java.
小結(jié)
本課時我們講了 8 種實現(xiàn)單例的方式,包括線程安全但可能會造成系統(tǒng)資源浪費的餓漢模式,以及懶漢模式和懶漢模式變種的 5 種實現(xiàn)方式。其中包含了兩種雙重檢測鎖的懶漢變種模式,還有最后兩種線程安全且可以實現(xiàn)延遲加載的靜態(tài)內(nèi)部類的實現(xiàn)方式和枚舉類的實現(xiàn)方式,其中比較推薦使用的是后兩種單例模式的實現(xiàn)方式。
