單例設(shè)計模式全解析
目錄:
1. 懶漢方式
1.1 非線程安全的單例模式
1.2 線程安全的單例模式
1.2.1 性能較差的同步單例模式
1.2.2 雙重檢查鎖
2. 餓漢方式
2.1 類加載實(shí)例化
3. 靜態(tài)內(nèi)部類方式
4. 枚舉方式
5. 總結(jié)
在學(xué)習(xí)設(shè)計模式時运杭,單例設(shè)計模式應(yīng)該是學(xué)習(xí)的第一個設(shè)計模式讲冠,單例設(shè)計模式也是“公認(rèn)”最簡單的設(shè)計模式券膀,但真實(shí)并非如此辟躏,本文將介紹了多種實(shí)現(xiàn)單例模式的方法谷扣。目前有三種方式可以實(shí)現(xiàn)單例模式,分別是
- 懶漢方式
- 餓漢方式
- 枚舉方式
1.懶漢方式
懶漢方式由懶加載的工作方式而得名捎琐。根據(jù)線程安全與否会涎,可以分為非線程安全和線程安全兩種實(shí)現(xiàn)方式。
1.1 非線程安全的單例模式
非線程安全的單例實(shí)現(xiàn)方式是學(xué)習(xí)單例模式時接觸到的第一個單例程序瑞凑。雖然該程序是非線程安全的末秃,但是能夠更好的理解單例模式的核心思想。具體的代碼如下:
代碼1:
public class Singleton {
//表示Singleton的唯一實(shí)例
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
//如果singleton的實(shí)例為null籽御,則新建實(shí)例练慕,否則返回創(chuàng)建好的實(shí)例對象
if( singleton == null ){
singleton = new Singleton();
}
return singleton;
}
}
從非線程安全的單例模式中,可以清楚看到技掏,單例模式包含了一個私有的構(gòu)造方法和一個靜態(tài)方法铃将,這是實(shí)現(xiàn)單例模式的必要條件。在Signleton類中哑梳,沒有同步操作劲阎,所以是線程不安全的。
1.2 線程安全的單例模式
為了實(shí)現(xiàn)線程安全的單例模式涧衙,一般通過synchronized關(guān)鍵字實(shí)現(xiàn)哪工,本次主要探討通過synchronized關(guān)鍵字實(shí)現(xiàn)奥此。
1.2.1 性能較差的單例模式
為了保證代碼1中Singleton類線程安全,可以為getInstance方法增加synchronized關(guān)鍵字修飾雁比。具體代碼如下:
代碼2:
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public synchronized static Singleton getInstance(){
//如果singleton的實(shí)例為null稚虎,則新建實(shí)例,否則返回創(chuàng)建好的實(shí)例對象
if( singleton == null ){
singleton = new Singleton();
}
return singleton;
}
}
在代碼2中實(shí)現(xiàn)了線程安全的getInstance方法偎捎,這樣保證了在任何時刻蠢终,只能有一個線程調(diào)用getInstance方法。但是這種卻是低效的茴她,因?yàn)楫?dāng)單例對象創(chuàng)建后寻拂,所有線程仍然無法同時調(diào)用getInstance方法,即使在這時線程安全問題已經(jīng)不存在丈牢。
既然為方法增加synchronized關(guān)鍵字會給程序性能帶來損失祭钉,那么有沒有一種方式可以避免呢?理想的情況應(yīng)該是當(dāng)singleton實(shí)例為null時己沛,才進(jìn)行同步操作慌核,否則直接返回singleton實(shí)例,這樣就大大降低了synchronize關(guān)鍵字帶來的性能損失申尼。
1.2.2 雙重檢查鎖
為了解決代碼2中的同步方法帶來的性能損失垮卓,依照1.2.1節(jié)最后提出的解決思路,本節(jié)主要介紹了雙重檢查鎖师幕,雙重檢查鎖實(shí)現(xiàn)只有當(dāng)單例沒有實(shí)例化時粟按,進(jìn)行同步,否則直接返回實(shí)例霹粥,不進(jìn)行同步灭将,從而降低了同步帶來的同步損失,具體如下述代碼所示:
代碼3:
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null ){ //A
synchronized (Singleton.class){ //B
if(singleton == null ){ //C
singleton=new Singleton(); //D
}
}
}
return singleton;
}
}
代碼3中通過雙重檢查鎖實(shí)現(xiàn)了線程安全的單例模式蒙挑,其中A行首先對singleton實(shí)例進(jìn)行是否為null的判斷宗侦,為了防止競爭,通過synchronized代碼塊實(shí)現(xiàn)同步忆蚀,程序是對Singleton Class 實(shí)現(xiàn)同步矾利,從而實(shí)現(xiàn)在任何一個時刻只能有一個類實(shí)例訪問同步塊代碼,在代碼內(nèi)部繼續(xù)進(jìn)行對singleton進(jìn)行了是否為null判斷馋袜,當(dāng)兩個條件同時滿足男旗,才新建實(shí)例,并且返回實(shí)例對象欣鳖。同時應(yīng)該注意類屬性Singleton由volatile關(guān)鍵字修飾察皇,這也是保證線程安全的關(guān)鍵部分,下面通過兩個問題,進(jìn)一步理解上述代碼:
-
為什么要進(jìn)行兩次NUll判斷什荣?請說明兩次NULL判斷存在的必要性矾缓。
答:首先需要說明,兩次非空判斷是為了保證在多線程的環(huán)境下實(shí)現(xiàn)線程安全稻爬。行A NULL判斷實(shí)現(xiàn)了如果singleton is not null 時嗜闻,直接返回實(shí)例。
為了進(jìn)一步說明桅锄,現(xiàn)假設(shè)存在線程1和線程2琉雳,在某一個時刻(singleton未被實(shí)例化),線程1運(yùn)行到了行C友瘤,線程2運(yùn)行到了A行翠肘。此時,線程2判斷singleton is null 從而進(jìn)入if體內(nèi)辫秧,由于沒有Singleton.class的同步鎖束倍,只能等待下去;接著盟戏,線程1判斷singleton同樣為null肌幽,繼續(xù)運(yùn)行D行并且返回了新建的實(shí)例,注意此時singleton已經(jīng)被實(shí)例化抓半。線程1結(jié)束,由于線程1釋放的同步鎖格嘁,從而線程2獲得了同步鎖笛求,繼續(xù)運(yùn)行同步塊內(nèi)的部分,假設(shè)行D的null判斷不存在糕簿,此時將返回新的Singleton對象探入,這樣就無法實(shí)現(xiàn)單例模式。所以兩次null判斷都是非常必要的懂诗。
-
請解釋一下volatile關(guān)鍵字的在程序中的作用
答:在多線程編程環(huán)境中蜂嗽,經(jīng)常會用到了volatile關(guān)鍵字,該關(guān)鍵字保證了每個線程在棧中不會保存該變量的副本殃恒,每次都是從主內(nèi)存中讀取該變量植旧,從而保證變量對于每個線程的可見性。然而volatile還有一個重要的特性:禁止指令的重排序優(yōu)化离唐。也就是說volatile變量的賦值操作會有個內(nèi)存屏障病附,讀操作不會被重排序到寫操作之前。
介紹了Volatile關(guān)鍵字的作用亥鬓,為了解釋volatile在程序中的作用完沪,首先解析一下行D,表面上行D只有一行代碼嵌戈,遺憾的是該行代碼不是原子性的覆积。事實(shí)上听皿,JVM虛擬機(jī)會解析成三個操作:
(1) 為singleton變量分配內(nèi)存;
(2) 調(diào)用Singleton構(gòu)造函數(shù)宽档,初始化成員變量,初始化Singleton的內(nèi)存空間
(3) singletion變量指向創(chuàng)建的Signleton的內(nèi)存空間
當(dāng)步驟(3)執(zhí)行完成后尉姨,singleton就不再是null。由于JVM內(nèi)部存在指令優(yōu)化雌贱,上述三個步驟的順序可能被打亂啊送,存在(1)(2)(3)和(1)(3)(2)的執(zhí)行順序。
假設(shè)存在線程1和線程2,在沒有volatile關(guān)鍵字修飾變量的情況下欣孤,線程1運(yùn)行至行D馋没,而線程2還沒有開始執(zhí)行。由上述分析可知行D被解析成三個原子操作降传,并且存在多種執(zhí)行順序篷朵,假設(shè)當(dāng)前的執(zhí)行順序是(1)(3)(2)。如果當(dāng)線程1執(zhí)行完(3)并且在(2)執(zhí)行之前婆排,線程2開始執(zhí)行可以看到singleton此時已經(jīng)指向某塊內(nèi)存空間声旺,不再是null,線程執(zhí)行行A然后就返回singleton段只,當(dāng)調(diào)用singleton就報錯了腮猖。這是因?yàn)閟ingleton所指向的內(nèi)存空間其實(shí)還不是Singleton的內(nèi)存空間,并沒有進(jìn)行初始化操作赞枕。
當(dāng)存在volatile關(guān)鍵字時澈缺,volatile保證了三個操作執(zhí)行完畢后,才允許進(jìn)行讀操作(讀操作不會被重排序到寫操作之前)炕婶,從而避免了上述可能出現(xiàn)的錯誤姐赡。
2. 餓漢方式
相比于懶漢方式的懶加載方式,餓漢方式就是另外一種非懶加載的方式柠掂。本節(jié)將詳細(xì)介紹餓漢方式中的類加載實(shí)例化方式项滑。廢話不多說,請看下面的代碼:
代碼4:
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
在代碼4中涯贞,最明顯的不同是靜態(tài)方法getInstance()中的判斷邏輯幾乎沒有了枪狂,代碼顯得更加的簡潔。但是也應(yīng)該注意到肩狂,singleton的實(shí)例化操作放在了屬性聲明的位置摘完,如果對static關(guān)鍵字非常了解的話,就應(yīng)該知道屬性singleton的實(shí)例化操作是當(dāng)Singleton類首次被加載(使用)時完成的傻谁,也就是說即使不調(diào)用getInstance方法孝治,singleton的仍然被實(shí)例化,并一直放在了堆內(nèi)存中。
雖然類加載實(shí)例化的方式使得代碼的判斷邏輯簡單了許多谈飒,但是該方式仍然有一個明顯的缺陷就是:即使程序中不需要單例對象岂座,只要單例類被加載到內(nèi)存中,單例對象就一直在內(nèi)存中存在杭措,如果內(nèi)存相對稀缺的話费什,那么將是災(zāi)難性的。
3. 靜態(tài)內(nèi)部類方式
為了解決餓漢方式所帶類的問題手素,本節(jié)將詳細(xì)地介紹靜態(tài)內(nèi)部類方式鸳址。具體的代碼如下所示:
代碼5:
public class Singleton {
private static class SingletonHolder{
private static final Singleton singleton = new Singleton();
}
private Singleton(){}
public static Singleton2 getInstance(){
return SingletonHolder.singleton;
}
}
代碼5中使用了靜態(tài)內(nèi)部類的方式實(shí)現(xiàn)了調(diào)用時的實(shí)例化方式,即是只有當(dāng)getInstance()方法被調(diào)用時泉懦,才實(shí)例化Singleton的實(shí)例稿黍。由于單例對象是當(dāng)調(diào)用時才進(jìn)行實(shí)例化,該方式其實(shí)是屬于懶漢方式崩哩。
4. 枚舉方式
枚舉是為了描述有限個狀態(tài)的數(shù)據(jù)結(jié)構(gòu)巡球,根據(jù)《JAVA編程思想》的介紹,枚舉類與普通類基本相同邓嘹,只是枚舉類中的幾個有限的值都是該枚舉類的靜態(tài)實(shí)例化對象酣栈。更多關(guān)于枚舉類的相關(guān)信息,可以深入閱讀《JAVA編程思想》的相關(guān)章節(jié)汹押。下面介紹一種方式實(shí)現(xiàn)矿筝,具體請看下面的代碼:
代碼6:
public enum Singleton {
INSTANCE;
}
代碼6是最簡單的枚舉,可以通過Singleton.INSTANCE訪問單例實(shí)例棚贾。如果想要為INSTANCE增加更多的“功能”跋涣,可以在枚舉類Singleton增加相關(guān)的方法,通過Singleton.INSTANTCE.methodName()來調(diào)用鸟悴。由于創(chuàng)建枚舉是線程安全的,所以沒有必要擔(dān)心線程安全問題奖年,并且還可以防止反序列化創(chuàng)建新的對象细诸。這也是《Effective Java》中推薦使用的單例方式。
5. 總結(jié)
本文主要對單例模式進(jìn)行全面的解析陋守,以逐步遞進(jìn)的方式介紹各種實(shí)現(xiàn)單例模式的方式震贵,依次介紹了非線程安全的懶漢方式、兩種線程安全的懶漢方式(性能損失的單例模式和雙重檢查鎖)水评、餓漢方式猩系、靜態(tài)內(nèi)部方式、枚舉方式中燥。在實(shí)際工作中寇甸,可以選用靜態(tài)內(nèi)部類和枚舉類方式來實(shí)現(xiàn)單例模式。