你真的會(huì)寫(xiě)單例嗎拾并?

你真的會(huì)寫(xiě)單例嗎蹦渣?

摘錄來(lái)源

單例的正確姿勢(shì)

Java單例模式可能是最簡(jiǎn)單也是最常用的設(shè)計(jì)模式,一個(gè)完美的單例需要做到哪些事呢樟蠕?

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

可以看到贮聂,真正要實(shí)現(xiàn)一個(gè)完美的單例是很復(fù)雜的靠柑,那么,讓我這個(gè)司機(jī)帶大家看一看正確姿勢(shì)的單例吓懈。

最佳實(shí)踐單例之枚舉

沒(méi)錯(cuò)歼冰,直接就上最佳實(shí)踐,就是這么任性

這貨長(zhǎng)這樣:

public enum Singleton{  
    INSTANCE;  
}    

如果你不熟悉枚舉耻警,可能會(huì)說(shuō):這貨是啥隔嫡?!

這種方式的好處是:

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

調(diào)用方式為Singleton.INSTANCE, 出自《Effective Java》第二版第三條: 用私有構(gòu)造器或枚舉類型強(qiáng)化Singleton屬性甘穿。
關(guān)于單例最佳實(shí)踐的討論可以看Stackoverflow:what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java

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

最簡(jiǎn)單的單例之餓漢式

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

這種單例的寫(xiě)法最簡(jiǎn)單温兼,但是缺點(diǎn)是一旦類被加載秸滴,單例就會(huì)初始化,沒(méi)有實(shí)現(xiàn)懶加載募判。而且當(dāng)實(shí)現(xiàn)了Serializable接口后荡含,反序列化時(shí)單例會(huì)被破壞。
實(shí)現(xiàn)Serializable接口需要重寫(xiě)readResolve届垫,才能保證其反序列化依舊是單例:

public class Singleton implements Serializable {  
    private static final Singleton INSTANCE = new Singleton();  
    // 私有化構(gòu)造函數(shù)  
    private Singleton(){}  
  
    public static Singleton getInstance(){  
        return INSTANCE;  
    }  
  
    /** 
     * 如果實(shí)現(xiàn)了Serializable, 必須重寫(xiě)這個(gè)方法 
     */  
    private Object readResolve() throws ObjectStreamException {  
        return INSTANCE;  
    }  
}    

OK内颗,反序列化要注意的就是這一點(diǎn),下面的內(nèi)容中就不再?gòu)?fù)述了敦腔。

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

懶漢式即實(shí)現(xiàn)延遲加載的單例均澳,為上述餓漢式的優(yōu)化形式。而因其仍需要進(jìn)一步優(yōu)化符衔,往往成為面試考點(diǎn)找前,讓我們一起來(lái)看看坑爹的“懶漢式”
懶漢式的最初形式是這樣的:

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

這種寫(xiě)法就輕松實(shí)現(xiàn)了單例的懶加載,只有調(diào)用了getInstance方法才會(huì)初始化判族。但是這樣的寫(xiě)法出現(xiàn)了新的問(wèn)題--線程不安全躺盛。當(dāng)多個(gè)線程調(diào)用getInstance方法時(shí),可能會(huì)創(chuàng)建多個(gè)實(shí)例形帮,因此需要對(duì)其進(jìn)行同步槽惫。

如何使其線程安全呢?簡(jiǎn)單辩撑,加個(gè)synchronized關(guān)鍵字就行了

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

可是...這樣又出現(xiàn)了性能問(wèn)題界斜,簡(jiǎn)單粗暴的同步整個(gè)方法,導(dǎo)致同一時(shí)間內(nèi)只有一個(gè)線程能夠調(diào)用getInstance方法合冀。

因?yàn)閮H僅需要對(duì)初始化部分的代碼進(jìn)行同步各薇,所以再次進(jìn)行優(yōu)化:

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

執(zhí)行兩次檢測(cè)很有必要:當(dāng)多線程調(diào)用時(shí),如果多個(gè)線程同時(shí)執(zhí)行完了第一次檢查君躺,其中一個(gè)進(jìn)入同步代碼塊創(chuàng)建了實(shí)例峭判,后面的線程因第二次檢測(cè)不會(huì)創(chuàng)建新實(shí)例开缎。
這段代碼看起來(lái)很完美,但仍舊存在問(wèn)題林螃,以下內(nèi)容引用自黑桃?jiàn)A克大神的如何正確地寫(xiě)出單例模式

這段代碼看起來(lái)很完美奕删,很可惜,它是有問(wèn)題疗认。主要在于instance = new Singleton()這句完残,這并非是一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情侮邀。

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

但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化坏怪。也就是說(shuō)上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2绊茧。如果是后者铝宵,則在 3 執(zhí)行完畢、2 未執(zhí)行之前华畏,被線程二搶占了鹏秋,這時(shí) instance 已經(jīng)是非 null 了(但卻沒(méi)有初始化),所以線程二會(huì)直接返回 instance亡笑,然后使用侣夷,然后順理成章地報(bào)錯(cuò)。
我們只需要將 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 的主要原因是其另一個(gè)特性:禁止指令重排序優(yōu)化百拓。也就是說(shuō),在 volatile 變量的賦值操作后面會(huì)有一個(gè)內(nèi)存屏障(生成的匯編代碼上)晰甚,讀操作不會(huì)被重排序到內(nèi)存屏障之前衙传。比如上面的例子,取操作必須在執(zhí)行完 1-2-3 之后或者 1-3-2 之后厕九,不存在執(zhí)行到 1-3 然后取到值的情況蓖捶。從「先行發(fā)生原則」的角度理解的話,就是對(duì)于一個(gè) volatile 變量的寫(xiě)操作都先行發(fā)生于后面對(duì)這個(gè)變量的讀操作(這里的“后面”是時(shí)間上的先后順序)扁远。

但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問(wèn)題的俊鱼。其原因是 Java 5 以前的 JMM (Java 內(nèi)存模型)是存在缺陷的,即時(shí)將變量聲明成 volatile 也不能完全避免重排序畅买,主要是 volatile 變量前后的代碼仍然存在重排序問(wèn)題并闲。這個(gè) volatile 屏蔽重排序的問(wèn)題在 Java 5 中才得以修復(fù),所以在這之后才可以放心使用 volatile皮获。

至此焙蚓,這樣的懶漢式才是沒(méi)有問(wèn)題的懶漢式。

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

public class Singleton {   
    /**  
     * 類級(jí)的內(nèi)部類洒宝,也就是靜態(tài)的成員式內(nèi)部類购公,該內(nèi)部類的實(shí)例與外部類的實(shí)例沒(méi)有綁定關(guān)系,  
     * 而且只有被調(diào)用到才會(huì)裝載雁歌,從而實(shí)現(xiàn)了延遲加載  
     */   
    private static class SingletonHolder{   
        /**  
         * 靜態(tài)初始化器宏浩,由JVM來(lái)保證線程安全  
         */   
        private static final Singleton instance = new Singleton();   
    }   
    /**  
     * 私有化構(gòu)造方法  
     */   
    private Singleton(){   
    }   
  
    public static  Singleton getInstance(){   
        return SingletonHolder.instance;   
    }   
}  

使用內(nèi)部類來(lái)維護(hù)單例的實(shí)例,當(dāng)Singleton被加載時(shí)靠瞎,其內(nèi)部類并不會(huì)被初始化比庄,故可以確保當(dāng) Singleton類被載入JVM時(shí),不會(huì)初始化單例類乏盐。只有 getInstance() 方法調(diào)用時(shí)佳窑,才會(huì)初始化 instance。同時(shí)父能,由于實(shí)例的建立是時(shí)在類加載時(shí)完成神凑,故天生對(duì)多線程友好,getInstance() 方法也無(wú)需使用同步關(guān)鍵字何吝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末溉委,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子爱榕,更是在濱河造成了極大的恐慌瓣喊,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件黔酥,死亡現(xiàn)場(chǎng)離奇詭異藻三,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)跪者,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)棵帽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人坑夯,你說(shuō)我怎么就攤上這事岖寞。” “怎么了柜蜈?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵仗谆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我淑履,道長(zhǎng)隶垮,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任秘噪,我火速辦了婚禮狸吞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己蹋偏,他們只是感情好便斥,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著威始,像睡著了一般枢纠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上黎棠,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天晋渺,我揣著相機(jī)與錄音,去河邊找鬼脓斩。 笑死木西,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的随静。 我是一名探鬼主播八千,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼挪挤!你這毒婦竟也來(lái)了叼丑?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤扛门,失蹤者是張志新(化名)和其女友劉穎鸠信,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體论寨,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡星立,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了葬凳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绰垂。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖火焰,靈堂內(nèi)的尸體忽然破棺而出劲装,到底是詐尸還是另有隱情,我是刑警寧澤昌简,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布占业,位于F島的核電站,受9級(jí)特大地震影響纯赎,放射性物質(zhì)發(fā)生泄漏谦疾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一犬金、第九天 我趴在偏房一處隱蔽的房頂上張望念恍。 院中可真熱鬧六剥,春花似錦、人聲如沸峰伙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)词爬。三九已至秃嗜,卻和暖如春权均,著一層夾襖步出監(jiān)牢的瞬間顿膨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工独旷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斜筐,地道東北人橄镜。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像囊咏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子塔橡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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

  • 前言 本文主要參考 那些年梅割,我們一起寫(xiě)過(guò)的“單例模式”。 何為單例模式葛家? 顧名思義户辞,單例模式就是保證一個(gè)類僅有一個(gè)...
    tandeneck閱讀 2,488評(píng)論 1 8
  • 1 場(chǎng)景問(wèn)題# 1.1 讀取配置文件的內(nèi)容## 考慮這樣一個(gè)應(yīng)用,讀取配置文件的內(nèi)容癞谒。 很多應(yīng)用項(xiàng)目底燎,都有與應(yīng)用相...
    七寸知架構(gòu)閱讀 6,675評(píng)論 12 68
  • 單例模式(SingletonPattern)一般被認(rèn)為是最簡(jiǎn)單、最易理解的設(shè)計(jì)模式弹砚,也因?yàn)樗暮?jiǎn)潔易懂双仍,是項(xiàng)目中最...
    成熱了閱讀 4,231評(píng)論 4 34
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司桌吃,掛了不少朱沃,但最終還是拿到小米、百度茅诱、阿里逗物、京東、新浪让簿、CVTE敬察、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,209評(píng)論 11 349
  • 我的天空里沒(méi)有太陽(yáng),總是黑夜尔当,但并不暗莲祸,因?yàn)橛袞|西代替了太陽(yáng)蹂安。雖然沒(méi)有太陽(yáng)那么明亮,但對(duì)我來(lái)說(shuō)已經(jīng)足夠锐帜。憑借著這份...
    呆小逗閱讀 122評(píng)論 0 0