老司機來教你單例的正確姿勢

老司機來教你單例的正確姿勢

Java單例模式可能是最簡單也是最常用的設計模式,一個完美的單例需要做到哪些事呢娜氏?

  1. 單例(這不是廢話嗎)
  2. 延遲加載
  3. 線程安全
  4. 沒有性能問題
  5. 防止序列化產(chǎn)生新對象
  6. 防止反射攻擊

可以看到,真正要實現(xiàn)一個完美的單例是很復雜的,那么详炬,讓我這個司機帶大家看一看正確姿勢的單例梨水。

最佳實踐單例之枚舉

沒錯赋铝,直接就上最佳實踐泼菌,就是這么任性

這貨長這樣:

public enum Singleton{
    INSTANCE;
}

如果你不熟悉枚舉,可能會說:這貨是啥琅关?颂砸!

這種方式的好處是:

  1. 利用的枚舉的特性實現(xiàn)單例
  2. 由JVM保證線程安全
  3. 序列化和反射攻擊已經(jīng)被枚舉解決

調(diào)用方式為Singleton.INSTANCE, 出自《Effective Java》第二版第三條: 用私有構(gòu)造器或枚舉類型強化Singleton屬性。

關于單例最佳實踐的討論可以看Stackoverflow:what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java

下面將會介紹更為常見的單例模式,但是均未處理反射攻擊人乓,如果想了解更多可以看這篇文章:如何防止單例模式被JAVA反射攻擊

最簡單的單例之餓漢式

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    // 私有化構(gòu)造函數(shù)
    private Singleton(){}

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

這種單例的寫法最簡單勤篮,但是缺點是一旦類被加載,單例就會初始化色罚,沒有實現(xiàn)懶加載碰缔。而且當實現(xiàn)了Serializable接口后,反序列化時單例會被破壞戳护。

實現(xiàn)Serializable接口需要重寫readResolve金抡,才能保證其反序列化依舊是單例:

public class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();
    // 私有化構(gòu)造函數(shù)
    private Singleton(){}

    public static Singleton getInstance(){
        return INSTANCE;
    }
        
    /**
     * 如果實現(xiàn)了Serializable, 必須重寫這個方法
     */
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

OK,反序列化要注意的就是這一點腌且,下面的內(nèi)容中就不再復述了梗肝。

最體現(xiàn)技術的單例之懶漢式

懶漢式即實現(xiàn)延遲加載的單例,為上述餓漢式的優(yōu)化形式铺董。而因其仍需要進一步優(yōu)化巫击,往往成為面試考點,讓我們一起來看看坑爹的“懶漢式”

懶漢式的最初形式是這樣的:

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

這種寫法就輕松實現(xiàn)了單例的懶加載精续,只有調(diào)用了getInstance方法才會初始化坝锰。但是這樣的寫法出現(xiàn)了新的問題--線程不安全。當多個線程調(diào)用getInstance方法時重付,可能會創(chuàng)建多個實例顷级,因此需要對其進行同步。

如何使其線程安全呢确垫?簡單弓颈,加個synchronized關鍵字就行了

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

可是...這樣又出現(xiàn)了性能問題,簡單粗暴的同步整個方法删掀,導致同一時間內(nèi)只有一個線程能夠調(diào)用getInstance方法恨豁。

因為僅僅需要對初始化部分的代碼進行同步,所以再次進行優(yōu)化:

public static Singleton getSingleton() {
    if (INSTANCE == null) {              // 第一次檢查
        synchronized (Singleton.class) {
            if (INSTANCE == null) {      // 第二次檢查
                INSTANCE = new Singleton();
            }
        }
    }
    return INSTANCE ;
}

執(zhí)行兩次檢測很有必要:當多線程調(diào)用時爬迟,如果多個線程同時執(zhí)行完了第一次檢查,其中一個進入同步代碼塊創(chuàng)建了實例菊匿,后面的線程因第二次檢測不會創(chuàng)建新實例付呕。

這段代碼看起來很完美,但仍舊存在問題跌捆,以下內(nèi)容引用自黑桃夾克大神的如何正確地寫出單例模式

這段代碼看起來很完美徽职,很可惜,它是有問題佩厚。主要在于instance = new Singleton()這句姆钉,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。

  1. 給 instance 分配內(nèi)存
  2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
  3. 將instance對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)

但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化潮瓶。也就是說上面的第二步和第三步的順序是不能保證的陶冷,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者毯辅,則在 3 執(zhí)行完畢埂伦、2 未執(zhí)行之前,被線程二搶占了思恐,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化)沾谜,所以線程二會直接返回 instance,然后使用胀莹,然后順理成章地報錯基跑。

我們只需要將 instance 變量聲明成 volatile 就可以了。

public class Singleton {
    private volatile static Singleton INSTANCE; //聲明成 volatile
    private Singleton (){}

    public static Singleton getSingleton() {
        if (INSTANCE == null) {                         
            synchronized (Singleton.class) {
                if (INSTANCE == null) {       
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
   
}

使用 volatile 的主要原因是其另一個特性:禁止指令重排序優(yōu)化描焰。也就是說媳否,在 volatile 變量的賦值操作后面會有一個內(nèi)存屏障(生成的匯編代碼上),讀操作不會被重排序到內(nèi)存屏障之前栈顷。比如上面的例子逆日,取操作必須在執(zhí)行完 1-2-3 之后或者 1-3-2 之后,不存在執(zhí)行到 1-3 然后取到值的情況萄凤。從「先行發(fā)生原則」的角度理解的話室抽,就是對于一個 volatile 變量的寫操作都先行發(fā)生于后面對這個變量的讀操作(這里的“后面”是時間上的先后順序)。

但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的靡努。其原因是 Java 5 以前的 JMM (Java 內(nèi)存模型)是存在缺陷的坪圾,即時將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前后的代碼仍然存在重排序問題惑朦。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復兽泄,所以在這之后才可以放心使用 volatile。

至此漾月,這樣的懶漢式才是沒有問題的懶漢式病梢。

內(nèi)部類實現(xiàn)單例

public class Singleton { 
    /** 
     * 類級的內(nèi)部類,也就是靜態(tài)的成員式內(nèi)部類梁肿,該內(nèi)部類的實例與外部類的實例沒有綁定關系蜓陌, 
     * 而且只有被調(diào)用到才會裝載,從而實現(xiàn)了延遲加載 
     */ 
    private static class SingletonHolder{ 
        /** 
         * 靜態(tài)初始化器吩蔑,由JVM來保證線程安全 
         */ 
        private static final Singleton instance = new Singleton(); 
    } 
    /** 
     * 私有化構(gòu)造方法 
     */ 
    private Singleton(){ 
    } 
     
    public static  Singleton getInstance(){ 
        return SingletonHolder.instance; 
    } 
} 

使用內(nèi)部類來維護單例的實例钮热,當Singleton被加載時,其內(nèi)部類并不會被初始化烛芬,故可以確保當 Singleton類被載入JVM時隧期,不會初始化單例類飒责。只有 getInstance() 方法調(diào)用時,才會初始化 instance仆潮。同時宏蛉,由于實例的建立是時在類加載時完成,故天生對多線程友好鸵闪,getInstance() 方法也無需使用同步關鍵字檐晕。

總結(jié)

無疑,單例就應使用枚舉實現(xiàn)蚌讼,最佳實踐誠不欺我

參考鏈接

What is an efficient way to implement a singleton pattern in Java

Java Practices -> Singleton

Creating and Destroying Java Objects: Part 1

如何正確地寫出單例模式

JAVA 枚舉單例模式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辟灰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子篡石,更是在濱河造成了極大的恐慌芥喇,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凰萨,死亡現(xiàn)場離奇詭異继控,居然都是意外死亡,警方通過查閱死者的電腦和手機胖眷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門武通,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人珊搀,你說我怎么就攤上這事冶忱。” “怎么了境析?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵囚枪,是天一觀的道長。 經(jīng)常有香客問我劳淆,道長链沼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任沛鸵,我火速辦了婚禮括勺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘曲掰。我一直安慰自己疾捍,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布蜈缤。 她就那樣靜靜地躺著,像睡著了一般冯挎。 火紅的嫁衣襯著肌膚如雪底哥。 梳的紋絲不亂的頭發(fā)上咙鞍,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音趾徽,去河邊找鬼续滋。 笑死,一個胖子當著我的面吹牛孵奶,可吹牛的內(nèi)容都是我干的疲酌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼了袁,長吁一口氣:“原來是場噩夢啊……” “哼朗恳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起载绿,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤粥诫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后崭庸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體怀浆,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年怕享,在試婚紗的時候發(fā)現(xiàn)自己被綠了执赡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡函筋,死狀恐怖沙合,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情驻呐,我是刑警寧澤灌诅,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站含末,受9級特大地震影響猜拾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜佣盒,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一挎袜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧肥惭,春花似錦盯仪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至牵囤,卻和暖如春爸黄,著一層夾襖步出監(jiān)牢的瞬間滞伟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工炕贵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留梆奈,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓称开,卻偏偏與公主長得像亩钟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鳖轰,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

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