設(shè)計模式之單例模式

單例設(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. 懶漢方式
  2. 餓漢方式
  3. 枚舉方式

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)單例模式。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拿霉,隨后出現(xiàn)的幾起案子吟秩,更是在濱河造成了極大的恐慌,老刑警劉巖绽淘,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涵防,死亡現(xiàn)場離奇詭異,居然都是意外死亡沪铭,警方通過查閱死者的電腦和手機(jī)壮池,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杀怠,“玉大人椰憋,你說我怎么就攤上這事⊥匀猓” “怎么了熏矿?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長离钝。 經(jīng)常有香客問我票编,道長,這世上最難降的妖魔是什么卵渴? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任慧域,我火速辦了婚禮,結(jié)果婚禮上浪读,老公的妹妹穿的比我還像新娘昔榴。我一直安慰自己,他們只是感情好碘橘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布互订。 她就那樣靜靜地躺著,像睡著了一般痘拆。 火紅的嫁衣襯著肌膚如雪仰禽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天纺蛆,我揣著相機(jī)與錄音吐葵,去河邊找鬼。 笑死桥氏,一個胖子當(dāng)著我的面吹牛温峭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播字支,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼凤藏,長吁一口氣:“原來是場噩夢啊……” “哼奸忽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起清笨,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤月杉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抠艾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苛萎,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年检号,在試婚紗的時候發(fā)現(xiàn)自己被綠了腌歉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡齐苛,死狀恐怖翘盖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凹蜂,我是刑警寧澤馍驯,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站玛痊,受9級特大地震影響汰瘫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜擂煞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一混弥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧对省,春花似錦蝗拿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至劳秋,卻和暖如春萤捆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背俗批。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留市怎,地道東北人岁忘。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像区匠,于是被迫代替她去往敵國和親干像。 傳聞我的和親對象是個殘疾皇子帅腌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容