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

定義:

保證一個(gè)類僅有一個(gè)實(shí)例, 并提供一個(gè)全局訪問(wèn)點(diǎn)

類型:

創(chuàng)建型

使用場(chǎng)景

  • 確保任何情況下都絕對(duì)只有一個(gè)實(shí)例

coding

單例模式需要注意的點(diǎn)
  1. 私有構(gòu)造器
  2. 線程安全
  3. 延遲加載
  4. 序列化和反序列化安全
  5. 防止反射機(jī)制破壞單例模式

單例模式的N種寫法

1. 餓漢式
  • 實(shí)現(xiàn)簡(jiǎn)單
  • 線程安全
public class HungrySingleton {
    private static HungrySingleton INSTANCE = new HungrySingleton();
    private HungrySingleton(){}
    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}

初始化類時(shí)就加載, 如果不使用就會(huì)浪費(fèi)內(nèi)存

2. 懶漢式
  • 實(shí)現(xiàn)簡(jiǎn)單
  • 延遲加載
public class LazySingleton {
    private static LazySingleton INSTANCE;
    private LazySingleton(){}
    public static LazySingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySingleton();
        }
        return INSTANCE;
    }
}

懶漢式的優(yōu)點(diǎn)是延遲加載,等到需要的時(shí)候才會(huì)創(chuàng)建實(shí)例, 但他是線程不安全的, 當(dāng)兩個(gè)線程同時(shí)進(jìn)入getInstance方法時(shí), 線程1和2都執(zhí)行到
INSTANCE == null, 此時(shí)INSTANCE如果還未創(chuàng)建, 將會(huì)創(chuàng)建兩個(gè)實(shí)例

線程不安全可以通過(guò)多線程調(diào)試來(lái)復(fù)現(xiàn)
IDEA多線程調(diào)試

3. 懶漢式 + 同步鎖
  • 延遲加載
  • 線程安全
public class LazySyncSingleton {
    private static LazySyncSingleton INSTANCE;
    private LazySyncSingleton(){}
    public static synchronized LazySyncSingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySyncSingleton();
        }
        return INSTANCE;
    }
}

這樣子線程就安全了,但是消耗了不必要的同步資源彬呻,不推薦這樣使用挡育。

4. DCL模式(Double CheckLock) - 雙重檢查
  • 延遲加載
  • 線程安全
  • 相對(duì)懶漢式 + 同步鎖的方式只在初始化時(shí)才會(huì)加鎖, 提高了效率
public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton INSTANCE;
    private LazyDoubleCheckSingleton(){}
    public static synchronized LazyDoubleCheckSingleton getInstance() {
        if (INSTANCE == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new LazyDoubleCheckSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

通過(guò)兩個(gè)判斷讲仰,第一層是避免不必要的同步楞慈,第二層判斷是否為null膊存。
可能會(huì)出現(xiàn)DCL模式失效的情況赘淮。

DCL模式失效:
singleton=new Singleton()這句話執(zhí)行的時(shí)候岔帽,會(huì)進(jìn)行下列三個(gè)過(guò)程:

  1. 分配內(nèi)存。
  2. 初始化構(gòu)造函數(shù)和成員變量退渗。
  3. 將對(duì)象指向分配的空間脆炎。

由于JMM(Java Memory Model)的規(guī)定,可能會(huì)對(duì)單線程情況下不影響程序運(yùn)行結(jié)果的指令進(jìn)行重排序, 因此可能會(huì)出現(xiàn)1-2-3和1-3-2兩種情況氓辣。
所以,就會(huì)出現(xiàn)線程A進(jìn)行到1-3時(shí)袱蚓,就被線程B取走钞啸,此時(shí)B線程拿到的是一個(gè)還未初始化完成的對(duì)象, 這時(shí)就出現(xiàn)了異常, DCL模式就失效了。

可以使用 volatile 來(lái)解決重排序問(wèn)題

volatile 有禁止指令重排序的功能. volatile詳解

private volatile static LazyDoubleCheckSingleton INSTANCE;
5.內(nèi)部類實(shí)現(xiàn)單例
  • 線程安全
  • 實(shí)現(xiàn)簡(jiǎn)單
  • 延遲加載
public class StaticInnerClassSingleton {
    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
    private StaticInnerClassSingleton(){}
    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

利用了class的初始化鎖保證只有一個(gè)線程能加載內(nèi)部類
只有通過(guò)顯式調(diào)用 getInstance 方法時(shí)喇潘,才會(huì)顯式裝載 SingletonHolder 類体斩,從而實(shí)例化 instance (只有拿到初始化鎖的線程才會(huì)初始化對(duì)象)

6.枚舉
  • 實(shí)現(xiàn)簡(jiǎn)單
  • 線程安全
  • 避免反序列化破壞單例
  • 避免反射攻擊
public enum EnumSingleton {
    INSTANCE(new Object());
    EnumSingleton(Object data) {
        this.data = data;
    }
    /**
     * 單例實(shí)體
     */
    private Object data;
    public Object getData() {
        return data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

Spring管理單例bean就是容器管理

7.容器
  • 統(tǒng)一管理單例
public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> CONTAINER = new HashMap<String,Object>();
    public static void putInstance(String key,Object instance){
        if(StringUtils.isNotBlank(key) && instance != null){
            if(!CONTAINER.containsKey(key)){
                CONTAINER.put(key,instance);
            }
        }
    }
    public static Object getInstance(String key){
        return CONTAINER.get(key);
    }
}
8.特殊的單例模式 ThreadLocal 實(shí)現(xiàn)線程單例
public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> INSTANCE
            = ThreadLocal.withInitial(ThreadLocalInstance::new);
    private ThreadLocalInstance(){
        System.out.println("init");
    }
    public static ThreadLocalInstance getInstance(){
        return INSTANCE.get();
    }
}

測(cè)試

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> INSTANCE
            = ThreadLocal.withInitial(ThreadLocalInstance::new);
    private ThreadLocalInstance(){
        System.out.println("init");
    }
    public static ThreadLocalInstance getInstance(){
        return INSTANCE.get();
    }
}

運(yùn)行, 在輸出結(jié)果中可以看到, 用一個(gè)線程獲取到的實(shí)例都是相同的, 即每個(gè)線程中只有一個(gè)實(shí)例存在, 在很多情況下是非常有用的,篇幅原因就不詳細(xì)展開了,想詳細(xì)了解的可以看一下這邊文章 => Java并發(fā)編程:深入剖析ThreadLocal

源碼中的單例

單例在源碼中是廣泛使用的
比如常用的工具類 java.lang.Math#random方法

public static double random() {
        return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
    }

這個(gè)RandomNumberGeneratorHolder.randomNumberGenerator是什么呢?

   private static final class RandomNumberGeneratorHolder {
        static final Random randomNumberGenerator = new Random();
    }

這正是上面提到的內(nèi)部類實(shí)現(xiàn)單例的模式.
其他的比如java.lang.Runtime

    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

一個(gè)非常明顯的餓漢式單例

序列化對(duì)單例模式的破壞

java中提供了對(duì)象的序列化與反序列化功能, 對(duì)象實(shí)現(xiàn)了Serializable接口之后就可以對(duì)對(duì)象的實(shí)例進(jìn)行序列化與反序列化, 下面以HungrySingleton 為例看一下 反序列化破壞單例模式的實(shí)例

public class SerializationBrokenSingletonTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        // 序列化
        oos.writeObject(instance);
        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        // 反序列化
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

運(yùn)行結(jié)果:

com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@568db2f2
false

明顯看到 反序列化之后, 得到了一個(gè)不同的對(duì)象實(shí)例.

在HungrySingleton中添加readResolve()方法

    private Object readResolve(){
        return hungrySingleton;
    }

再次運(yùn)行代碼

com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
true

神奇的發(fā)現(xiàn)返回true, 這是怎么回事呢,
有興趣的朋友可以debug跟蹤一下

  1. ObjectInputStream#readObject
  2. ObjectInputStream#readObject0
  3. ObjectInputStream#readOrdinaryObject

重點(diǎn)關(guān)注ObjectInputStream#readOrdinaryObject中的
obj = desc.isInstantiable() ? desc.newInstance() : null

Object rep = desc.invokeReadResolve(obj)
就可以知道 readResolve 的調(diào)用原理了.

反射對(duì)單例模式的破壞

public class ReflectBrokenSingletonTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        HungrySingleton instance = HungrySingleton.getInstance();
        Class<HungrySingleton> clazz = HungrySingleton.class;
        Constructor<HungrySingleton> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton newInstance = constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 對(duì)于在類加載階段就初始化的實(shí)例的單例模式(餓漢式, 內(nèi)部類)可以通過(guò)在構(gòu)造器中拋出異常的方式防止反射攻擊
    private HungrySingleton(){
        if(INSTANCE != null){
            throw new RuntimeException("單例構(gòu)造器禁止反射調(diào)用");
        }
    }

對(duì)于懶加載的單例模式(懶漢式, 懶漢式+同步鎖, DCL模式), 如果在構(gòu)造器中拋出異常的話, 當(dāng)實(shí)例在反射調(diào)用constructor.newInstance()執(zhí)行之前就已經(jīng)實(shí)例化時(shí), 是可以按照預(yù)期拋出異常的, 但是如果單例模式中的實(shí)例還未被實(shí)例化, 執(zhí)行constructor.newInstance()不會(huì)拋出異常, 因?yàn)榇藭r(shí)INSTANCE == null.

優(yōu)點(diǎn):

  • 內(nèi)存只有一個(gè)實(shí)例, 減少內(nèi)存開銷
  • 設(shè)置全局訪問(wèn)點(diǎn), 嚴(yán)格控制訪問(wèn)

缺點(diǎn):

  • 沒有接口, 擴(kuò)展困難

github源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市颖低,隨后出現(xiàn)的幾起案子絮吵,更是在濱河造成了極大的恐慌,老刑警劉巖忱屑,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹬敲,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡莺戒,警方通過(guò)查閱死者的電腦和手機(jī)伴嗡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)从铲,“玉大人瘪校,你說(shuō)我怎么就攤上這事。” “怎么了阱扬?”我有些...
    開封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵泣懊,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我麻惶,道長(zhǎng)馍刮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任用踩,我火速辦了婚禮渠退,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘脐彩。我一直安慰自己碎乃,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開白布惠奸。 她就那樣靜靜地躺著梅誓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪佛南。 梳的紋絲不亂的頭發(fā)上梗掰,一...
    開封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音嗅回,去河邊找鬼及穗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛绵载,可吹牛的內(nèi)容都是我干的埂陆。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼娃豹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼焚虱!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起懂版,我...
    開封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鹃栽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后躯畴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體民鼓,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年蓬抄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了摹察。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡倡鲸,死狀恐怖供嚎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤克滴,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布逼争,位于F島的核電站,受9級(jí)特大地震影響劝赔,放射性物質(zhì)發(fā)生泄漏誓焦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一着帽、第九天 我趴在偏房一處隱蔽的房頂上張望杂伟。 院中可真熱鬧,春花似錦仍翰、人聲如沸赫粥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)越平。三九已至,卻和暖如春灵迫,著一層夾襖步出監(jiān)牢的瞬間秦叛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工瀑粥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挣跋,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓狞换,卻偏偏與公主長(zhǎng)得像浆劲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哀澈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

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