設(shè)計(jì)模式 - 單例模式

在項(xiàng)目開發(fā)時(shí)有一些對(duì)象其實(shí)我們只需要一個(gè),比如:線程池娩井、緩存暇屋、日志對(duì)象等等。這類對(duì)象只能有一個(gè)實(shí)例洞辣,如果制造出多個(gè)實(shí)例咐刨,就會(huì)導(dǎo)致許多問題產(chǎn)生,例如:程序的行為異常扬霜,資源使用過量定鸟,或者是不一致的結(jié)果。

雖然程序員之間的約定以及全局變量也可以辦得到著瓶,但是單例模式確實(shí)是經(jīng)得起時(shí)間考驗(yàn)的更好的做法联予。單例模式和全局變量一樣方便的給我們提供了一個(gè)全局的訪問點(diǎn),但是也解決了全局變量必須在程序一開始就要?jiǎng)?chuàng)建好對(duì)象的缺點(diǎn)材原。單例模式可以靈活的決定對(duì)象什么時(shí)候創(chuàng)建沸久。

結(jié)構(gòu)定義

單例模式

單例模式: 保證一個(gè)類僅有一個(gè)實(shí)例,并且提供一個(gè)訪問它的全局訪問點(diǎn)余蟹。

通常我們可以讓一個(gè)全局變量使得一個(gè)對(duì)象被訪問卷胯,但它不能防止你實(shí)例化多個(gè)對(duì)象。一個(gè)最好的辦法就是讓類自身負(fù)責(zé)保存它的唯一實(shí)例威酒。這個(gè)類可以保證沒有其它實(shí)例可以被創(chuàng)建,并且它可以提供一個(gè)訪問該實(shí)例的方法窑睁。[DP]

單例模式的寫法(7種)

單例模式的思路

  1. 利用一個(gè)靜態(tài)變量INSTANCE來記錄類的唯一實(shí)例
  2. 把構(gòu)造器聲明為私有的挺峡,只有在類本身才能調(diào)用構(gòu)造器
  3. getInstance()方法實(shí)例化對(duì)象,并返回這個(gè)類的實(shí)例

分析:
利用靜態(tài)變量來保存類的實(shí)例確保該實(shí)例為類的唯一實(shí)例卵慰,如果實(shí)例為空沙郭,則表示還沒有創(chuàng)建實(shí)例,而如果不存在我們就利用私有的構(gòu)造器產(chǎn)生一個(gè)該類實(shí)例并把它賦值到靜態(tài)變量中裳朋,如果我們不需要這個(gè)實(shí)例病线,它就永遠(yuǎn)不會(huì)產(chǎn)生。這個(gè)就是延遲實(shí)例化

  • 懶漢模式(線程不安全)
public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

這段代碼簡(jiǎn)單明了鲤嫡,而且使用了懶加載模式送挑,但是卻存在致命的問題。當(dāng)有多個(gè)線程并行調(diào)用 getInstance() 的時(shí)候暖眼,就會(huì)創(chuàng)建多個(gè)實(shí)例惕耕。也就是說在多線程下不能正常工作。

  • 懶漢模式(線程安全)
    解決懶漢模式線程安全問題诫肠,最簡(jiǎn)單的方法是將整個(gè) getInstance() 方法設(shè)為同步synchronized司澎。
public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

雖然做到了線程安全,并且解決了多實(shí)例的問題栋豫,但是它并不高效挤安。因?yàn)樵谌魏螘r(shí)候只能有一個(gè)線程調(diào)用 getInstance()方法。但是同步操作只需要在第一次調(diào)用時(shí)才被需要丧鸯,即第一次創(chuàng)建單例實(shí)例對(duì)象時(shí)蛤铜。這就引出了雙重檢驗(yàn)鎖。

  • 雙重校驗(yàn)鎖 *
    雙重檢驗(yàn)鎖模式(double checked locking pattern)丛肢,是一種使用同步塊加鎖的方法围肥。程序員稱其為雙重檢查鎖,因?yàn)闀?huì)有兩次檢查 instance == null蜂怎,一次是在同步塊外穆刻,一次是在同步塊內(nèi)。為什么在同步塊內(nèi)還要再檢驗(yàn)一次派敷?因?yàn)榭赡軙?huì)有多個(gè)線程一起進(jìn)入同步塊外的 if蛹批,如果在同步塊內(nèi)不進(jìn)行二次檢驗(yàn)的話就會(huì)生成多個(gè)實(shí)例了。
 public static Singleton getInstance() {
        if (INSTANCE == null) {  // 一重
            synchronized (Singleton.class) {
                if (INSTANCE == null) { // 二重
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

這段代碼會(huì)有一個(gè)隱藏問題篮愉,主要是在INSTANCE = new Singleton()這句涉及到了JVM編譯器的指令重排,這并非是一個(gè)原子操作差导,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情:

  1. 給 instance 分配內(nèi)存
  2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
  3. 將instance對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)
    但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化试躏。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2设褐。如果是后者颠蕴,則在 3 執(zhí)行完畢泣刹、2 未執(zhí)行之前,被線程二搶占了犀被,這時(shí) instance 已經(jīng)是非 null 了(但卻沒有初始化)椅您,所以線程二會(huì)直接返回 instance,然后使用寡键,然后順理成章地報(bào)錯(cuò)掀泳。
    我們只需要將 instance 變量聲明成 volatile 就可以了。
public class Singleton {
    private static volatile Singleton INSTANCE;

    private Singleton() {
    }
    /**
     * 雙重校驗(yàn)鎖
     */
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

關(guān)于volatile修飾符用最簡(jiǎn)單的方式理解就是阻止了變量訪問前后的指令重排西轩,保證了指令執(zhí)行順序员舵。

  • 餓漢模式
public class Singleton {  
    private static final Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
        return instance;  
    }  
}  

這種方法非常簡(jiǎn)單,因?yàn)閱卫膶?shí)例被聲明成 staticfinal 變量了藕畔,在第一次加載類到內(nèi)存中時(shí)就會(huì)初始化马僻,所以創(chuàng)建實(shí)例本身是線程安全的。

這種方式基于classloder機(jī)制避免了多線程的同步問題注服,不過韭邓,instance在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種溶弟,在單例模式中大多數(shù)都是調(diào)用getInstance方法女淑, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化instance顯然沒有達(dá)到lazy loading的效果可很。

  • 餓漢模式(變種)
private static Singleton instance;

    static {
        instance = new Singleton();
    }

    public static Singleton getInstance() {
        return instance;
    }

這種寫法本質(zhì)上和上一種寫法沒什么區(qū)別诗力。

  • 靜態(tài)內(nèi)部類
 private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

注意:

  1. 從外部無法訪問靜態(tài)內(nèi)部類SingletonHolder,只有當(dāng)調(diào)用Singleton.getInstance方法的時(shí)候我抠,才能得到單例對(duì)象INSTANCE苇本。
  2. INSTANCE對(duì)象初始化的時(shí)機(jī)并不是在單例類Singleton被加載的時(shí)候,而是在調(diào)用getInstance方法菜拓,使得靜態(tài)內(nèi)部類SingletonHolder被加載的時(shí)候瓣窄。因此這種實(shí)現(xiàn)方式是利用classloader的加載機(jī)制來實(shí)現(xiàn)懶加載,并保證構(gòu)建單例的線程安全纳鼎。
  3. 無法防止利用反射重復(fù)構(gòu)建對(duì)象
  • 枚舉高效寫法
    在《Effective Java》最后推薦了這樣一個(gè)寫法俺夕,簡(jiǎn)直有點(diǎn)顛覆,不僅超級(jí)簡(jiǎn)單贱鄙,而且保證了線程安全劝贸。這里引用一下,此方法無償提供了序列化機(jī)制逗宁,絕對(duì)防止多次實(shí)例化映九,及時(shí)面對(duì)復(fù)雜的序列化或者反射攻擊。單元素枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法瞎颗。
public enum Singleton {
    /**
     *
     */
    INSTANCE;

    /**
     *
     */
    public void hello() {
        System.out.println("Hello World");
    }
}

對(duì)于一個(gè)標(biāo)準(zhǔn)的enum單例模式件甥,最優(yōu)秀的寫法還是實(shí)現(xiàn)接口的形式:

public enum Singleton implements MySingleton {
    /**
     *
     */
    INSTANCE {
        @Override
        public void hello() {
            System.out.println("Hello world");
        }
    }
}

interface MySingleton {
    /**
     * xx
     */
    void hello();
}

使用枚舉實(shí)現(xiàn)單例模式不僅防止了反射構(gòu)建對(duì)象也保證了線程安全捌议,但是同時(shí)它并不是懶加載,在枚舉類加載的同時(shí)引有,其單例對(duì)象就已經(jīng)被初始化瓣颅。

總結(jié)

單例模式寫法總結(jié)起來可以分為五種懶漢惡漢譬正、雙重校驗(yàn)鎖宫补、枚舉靜態(tài)內(nèi)部類导帝,上述所說都是線程安全的實(shí)現(xiàn)守谓,第一種應(yīng)該說是不正確的實(shí)現(xiàn)。
對(duì)于這幾種的比較

單例模式 是否線程安全 是否懶加載 是否防止反射構(gòu)建
雙重校驗(yàn)鎖
枚舉
靜態(tài)內(nèi)部類

補(bǔ)充

  1. volatile關(guān)鍵字不但可以防止指令重排您单,也可以保證線程訪問的變量值是主內(nèi)存中的最新值斋荞。有關(guān)volatile的詳細(xì)原理,我在以后的漫畫中會(huì)專門講解虐秦。
  2. 使用枚舉實(shí)現(xiàn)的單例模式平酿,不但可以防止利用反射強(qiáng)行構(gòu)建單例對(duì)象,而且可以在枚舉類對(duì)象被反序列化的時(shí)候悦陋,保證反序列的返回結(jié)果是同一對(duì)象蜈彼。
    對(duì)于其他方式實(shí)現(xiàn)的單例模式,如果既想要做到可序列化俺驶,又想要反序列化為同一對(duì)象幸逆,則必須實(shí)現(xiàn)readResolve方法。
  3. 應(yīng)該在任何情況下都應(yīng)實(shí)現(xiàn)線程安全的寫法暮现。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末还绘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子栖袋,更是在濱河造成了極大的恐慌拍顷,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件塘幅,死亡現(xiàn)場(chǎng)離奇詭異昔案,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)电媳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門踏揣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人匾乓,你說我怎么就攤上這事呼伸。” “怎么了钝尸?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵括享,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我珍促,道長(zhǎng)铃辖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任猪叙,我火速辦了婚禮娇斩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘穴翩。我一直安慰自己犬第,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布芒帕。 她就那樣靜靜地躺著歉嗓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪背蟆。 梳的紋絲不亂的頭發(fā)上鉴分,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音带膀,去河邊找鬼志珍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛垛叨,可吹牛的內(nèi)容都是我干的伦糯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼嗽元,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼敛纲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起还棱,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤载慈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后珍手,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體办铡,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年琳要,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寡具。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡稚补,死狀恐怖童叠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤厦坛,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布五垮,位于F島的核電站,受9級(jí)特大地震影響杜秸,放射性物質(zhì)發(fā)生泄漏放仗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一撬碟、第九天 我趴在偏房一處隱蔽的房頂上張望诞挨。 院中可真熱鬧,春花似錦呢蛤、人聲如沸惶傻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)银室。三九已至,卻和暖如春静秆,著一層夾襖步出監(jiān)牢的瞬間粮揉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工抚笔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扶认,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓殊橙,卻偏偏與公主長(zhǎng)得像辐宾,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子膨蛮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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