設(shè)計(jì)模式讀書筆記(二)單例模式

1. 單例模式的實(shí)現(xiàn)方式

1.1. 餓漢模式

// 最簡單的單例實(shí)現(xiàn)
public class Singleton {

    private static final Singleton instance = new Singleton();

    private Singleton() {

    }

    public static Singleton getInstance() {
        return instance;
    }
}
// 餓漢模式的一種變種
public class Singleton {

    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    private Singleton() {

    }

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

上面兩種實(shí)現(xiàn)方式的思想其實(shí)是一樣的陪拘,就是在類加載的時(shí)候?qū)嵗粋€(gè)對象茬底,這樣避免了線程安全的問題(關(guān)于線程安全問題在下面的例子中會提到)饿肺,但是這也可能會造成一些不必要的資源消耗,比如僅僅載入了類,這時(shí)候?qū)ο笠呀?jīng)實(shí)例化了匿又,但是有可能我們永遠(yuǎn)都用不到它。

1.2. 懶漢模式

public class Singleton {
    
    private static Singleton instance;
    
    private Singleton() {
        
    }

    // 同步方法
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懶漢模式的整體思想是當(dāng)需要一個(gè)單例對象的時(shí)候建蹄,判斷對象是否已經(jīng)實(shí)例化碌更,如果沒有,則進(jìn)行實(shí)例化洞慎,否則直接返回已經(jīng)實(shí)例化的對象痛单。這里用了「同步方法」,這樣可以保證「線程安全」劲腿,當(dāng)一個(gè)線程調(diào)用該方法時(shí)旭绒,另一個(gè)方法正好也要調(diào)用該方法,則需要等待前一個(gè)線程調(diào)用完畢,這樣就可以保證在多線程的情況下挥吵,對象也只會被實(shí)例化一次重父。
但是這種方式在性能上會有一些缺點(diǎn),因?yàn)槊看握{(diào)用 getInstance 方法都需要進(jìn)行同步忽匈,會造成不必要的同步開銷房午。

1.3 Double Check Lock (DCL)模式

public class Singleton {
    
    private static Singleton instance;

    private Singleton() {
        
    }

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

「雙重校驗(yàn)鎖」模式的重點(diǎn)就是雙重校驗(yàn),它通過兩次 instance == null 的判斷保證了線程安全丹允,同時(shí)避免了上一種「懶漢模式」的同步性能消耗郭厌。
我們一步一步進(jìn)行分析,假設(shè)現(xiàn)在有三個(gè)線程 A嫌松、B 和 C 都要使用單例對象沪曙,它們都去調(diào)用 getInstance 方法。

  1. 線程 A 先走到外層 if 處萎羔,判斷發(fā)現(xiàn) instance 還未實(shí)例化液走,此時(shí)就會進(jìn)入同步代碼塊。
  2. 與此同時(shí)贾陷,線程 B 也走到了外層 if 語句處缘眶,判斷發(fā)現(xiàn) instance 仍未實(shí)例化,進(jìn)行下一步髓废,但是由于此時(shí)線程 A 已經(jīng)獲得了同步鎖巷懈,正在執(zhí)行同步塊中的代碼,線程 B 需要等待慌洪。
  3. 線程 A 進(jìn)行內(nèi)層 if 判斷顶燕,發(fā)現(xiàn) instance 為 null,還沒有實(shí)例化冈爹,則對 instance 進(jìn)行實(shí)例化涌攻,然后退出同步塊,獲得 instance 對象频伤。
  4. 由于線程 A 退出同步塊恳谎,線程 B 獲得同步鎖,進(jìn)入同步塊憋肖,執(zhí)行其中的代碼因痛,進(jìn)行內(nèi)層 if 判斷,發(fā)現(xiàn) instance 對象已經(jīng)被實(shí)例化岸更,于是退出同步塊鸵膏,獲得 instance 對象。
  5. 線程 C 調(diào)用 getInstance怎炊,進(jìn)行外層 if 判斷较性,發(fā)現(xiàn) instance 已經(jīng)被實(shí)例化用僧,直接獲得 instance 對象,不再需要進(jìn)行同步操作赞咙。
    通過分析责循,我們可以得出 instance 兩次判空的用意,內(nèi)層判空是為了在 instance 還未實(shí)例化的時(shí)候創(chuàng)建實(shí)例攀操,而外層判斷是為了在 instance 已經(jīng)實(shí)例化的時(shí)候避免不必要的同步操作院仿。

然而,這種模式并不是最優(yōu)的模式速和,依然存在一些問題歹垫,我們繼續(xù)進(jìn)行分析。

首先我們需要知道颠放, instance = new Singleton() 這句代碼看似是一句代碼排惨,但是它最終會被編譯成多條匯編指令,大概做了下面 3 件事情:

  1. 為 Singleton 實(shí)例分配內(nèi)存碰凶。
  2. 調(diào)用構(gòu)造函數(shù)暮芭,初始化成員字段。
  3. 將 instance 對象引用指向分配的內(nèi)存空間欲低,經(jīng)歷這一步之后辕宏,instance 才不為 null。
    但是由于 Java 編譯器允許處理器亂序處理砾莱,也就是說瑞筐,步驟 2 和步驟 3 的順序是可以交換的,我們假設(shè)一種情況腊瑟,當(dāng)一個(gè)線程先執(zhí)行了步驟 1 和 3聚假,但是步驟 2 還沒有執(zhí)行,此時(shí) instance 已經(jīng)不為 null 了闰非,但成員字段還沒有完成初始化膘格,如果此時(shí)切換到另一個(gè)線程,在外層 if 判空的時(shí)候就會得到非空的結(jié)果河胎,從而獲得成員字段未初始化的 instance 對象闯袒,就會導(dǎo)致錯誤虎敦。

除了上面這一點(diǎn)游岳,還有一點(diǎn)就是在 JDK1.5 之前因?yàn)?Java 內(nèi)存模型中的 Cache、寄存器導(dǎo)主內(nèi)存會寫順序的規(guī)定其徙,也會導(dǎo)致這種 instance 非 null胚迫,但是成員字段未正確初始化的情況。

因此從 JDK1.5 開始唾那,官方調(diào)整了 JVM访锻,新增一個(gè) volatile 關(guān)鍵詞褪尝,只要在 instance 的定義前加上 volatile 關(guān)鍵字,也就是修改為 private volatile static Singleton instance; 就可以保證不會出現(xiàn)上述問題期犬。

1.4 靜態(tài)內(nèi)部類模式

public class Singleton {

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
    
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
}

這種方式是比較推薦的單例模式的實(shí)現(xiàn)方式河哑。這種方式其實(shí)是對餓漢式的一種升級,它很好地利用了內(nèi)部類來避免僅僅載入類龟虎,對象就已經(jīng)實(shí)例化的問題璃谨。只有在真正需要 instance 對象,調(diào)用 getInstance 方法的時(shí)候鲤妥,才會實(shí)例化對象佳吞。

1.5 枚舉單例

public enum SingletonEnum {
    INSTANCE;

    private int i;
    
    public void fun() {

    }
}

最簡單的單例模式實(shí)現(xiàn)方式就是使用枚舉了,其中的 INSTANCE 并沒有實(shí)際用途棉安,是因?yàn)楸仨氁幸粋€(gè)枚舉項(xiàng)底扳。枚舉在 Java 中和普通類是一樣的,可以有成員字段贡耽,也可以有成員方法衷模,而且使用枚舉不需要寫任何額外的代碼來保證線程安全。

之前提到的單例模式實(shí)現(xiàn)方法在一種特殊情況下仍會重復(fù)實(shí)例化對象菇爪,那就是在反序列化操作的時(shí)候算芯,為了避免出現(xiàn)這種情況,我們還需要在代碼中加入下面方法:

private Object readResolve()  throws ObjectStreamException {
    return instance;
}

而通過枚舉實(shí)現(xiàn)凳宙,這些都不需要考慮熙揍,即使是反序列化也不會重復(fù)實(shí)例化對象。

1.6 使用容器

public class SingletonManager {

    private static Map<String, Object> objMap = new HashMap<>();

    private SingletonManager() {
        
    }

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);
    }
}

使用容器來統(tǒng)一管理單例氏涩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末届囚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子是尖,更是在濱河造成了極大的恐慌意系,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饺汹,死亡現(xiàn)場離奇詭異蛔添,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)兜辞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門迎瞧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逸吵,你說我怎么就攤上這事凶硅。” “怎么了扫皱?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵足绅,是天一觀的道長捷绑。 經(jīng)常有香客問我,道長氢妈,這世上最難降的妖魔是什么粹污? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮首量,結(jié)果婚禮上厕怜,老公的妹妹穿的比我還像新娘。我一直安慰自己蕾总,他們只是感情好粥航,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著生百,像睡著了一般递雀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蚀浆,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天缀程,我揣著相機(jī)與錄音,去河邊找鬼市俊。 笑死杨凑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摆昧。 我是一名探鬼主播撩满,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绅你!你這毒婦竟也來了伺帘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤忌锯,失蹤者是張志新(化名)和其女友劉穎伪嫁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偶垮,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡张咳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了似舵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脚猾。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖啄枕,靈堂內(nèi)的尸體忽然破棺而出婚陪,到底是詐尸還是另有隱情族沃,我是刑警寧澤频祝,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布泌参,位于F島的核電站,受9級特大地震影響常空,放射性物質(zhì)發(fā)生泄漏沽一。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一漓糙、第九天 我趴在偏房一處隱蔽的房頂上張望铣缠。 院中可真熱鬧,春花似錦昆禽、人聲如沸蝗蛙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捡硅。三九已至,卻和暖如春盗棵,著一層夾襖步出監(jiān)牢的瞬間壮韭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工纹因, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留喷屋,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓瞭恰,卻偏偏與公主長得像屯曹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子惊畏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

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

  • 前言 本文主要參考 那些年是牢,我們一起寫過的“單例模式”。 何為單例模式陕截? 顧名思義驳棱,單例模式就是保證一個(gè)類僅有一個(gè)...
    tandeneck閱讀 2,515評論 1 8
  • 單例模式(SingletonPattern)一般被認(rèn)為是最簡單、最易理解的設(shè)計(jì)模式农曲,也因?yàn)樗暮啙嵰锥缃粒琼?xiàng)目中最...
    成熱了閱讀 4,254評論 4 34
  • 1 場景問題# 1.1 讀取配置文件的內(nèi)容## 考慮這樣一個(gè)應(yīng)用,讀取配置文件的內(nèi)容乳规。 很多應(yīng)用項(xiàng)目形葬,都有與應(yīng)用相...
    七寸知架構(gòu)閱讀 6,773評論 12 68
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法暮的,內(nèi)部類的語法笙以,繼承相關(guān)的語法,異常的語法冻辩,線程的語...
    子非魚_t_閱讀 31,639評論 18 399
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理猖腕,服務(wù)發(fā)現(xiàn)拆祈,斷路器,智...
    卡卡羅2017閱讀 134,660評論 18 139