設(shè)計模式-單例模式【實現(xiàn)成黄、序列化呐芥、反射】

設(shè)計模式-單例模式【實現(xiàn)、序列化奋岁、反射】

[toc]

1. 實現(xiàn)

單例模式的實現(xiàn)有很多種思瘟,分類方式也不一而足,比如分為預(yù)加載和懶加載闻伶,以及線程安全的實現(xiàn)及線程不安全的實現(xiàn)

1.1. 線程不安全

1.1.1 餓漢式

調(diào)用時判斷實例是否已經(jīng)初始化滨攻,沒有的話初始化并賦值。
優(yōu)點:

  1. 懶加載
  2. 運(yùn)行效率高

缺點:

  1. 非線程安全:實例未初始化時,如果有多個線程并發(fā)調(diào)用getInstance方法光绕,可能會造成各線程獲取到不同的實例

適用:如非確定不會被多線程調(diào)用更鲁,否則不建議使用

public static PlainNotSafe getInstance() {
        if (instance == null) {
            instance = new PlainNotSafe();
        }
        return instance;
    }

1.2. 線程安全

1.2.1 飽漢式

在類初始化過程中即進(jìn)行實例的創(chuàng)建:

優(yōu)點:

  1. 實現(xiàn)簡單
  2. 線程安全:實例初始化在類加載階段完成,JVM內(nèi)部保證此過程的線程安全性

缺點:

  1. 非懶加載

適用: 實例初始化耗費資源少奇钞,或者啟動時間不敏感澡为,或者業(yè)務(wù)要求啟動后快速響應(yīng)

class LoadAhead implements Singleton{
    private static LoadAhead instance = new LoadAhead();
    private LoadAhead(){}
    public static LoadAhead getInstance(){
        return instance;
    }
}

1.2.2 單同步鎖

使用syncronized關(guān)鍵字修飾getInstance方法

    public static synchronized LazyLoadWithOneSynchronization getInstance(){
        if (instance == null) {
            instance = new LazyLoadWithOneSynchronization();
        }
        return instance;
    }

優(yōu)點:

  1. 線程安全
  2. 實現(xiàn)簡單

缺點:

  1. 高并發(fā)環(huán)境,執(zhí)行效率低:同時只有一個線程可以獲取實例

適用: 不適用任何場景

1.2.3 雙重檢查+同步鎖

考慮到實例創(chuàng)建過程僅需要同步一次景埃,后面不需要同步媒至,因此只在實例未創(chuàng)建時進(jìn)行同步:

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

優(yōu)點:

  1. 線程安全
  2. 并發(fā)執(zhí)行效率高:僅在實例第一次創(chuàng)建過程有鎖競爭

缺點:

  1. 實現(xiàn)復(fù)雜

適用:對于不考慮序列化及反射破壞唯一性的場景,推薦使用此方法

1.2.4 內(nèi)部類

通過內(nèi)部類持有唯一實例谷徙,通過類加載機(jī)制保證懶加載和線程安全

/**
 * @Author: kkyeer
 * @Description: 懶漢式3拒啰,使用內(nèi)部類來進(jìn)行懶加載,原理是內(nèi)部類初始化時完慧,使用
 * @Date:Created in 14:57 2019/6/24
 * @Modified By:
 */
class LazyLoadWithInnerClass implements Singleton{
    private LazyLoadWithInnerClass(){}

    private static class Inner{
        static LazyLoadWithInnerClass instance = new LazyLoadWithInnerClass();
    }

    public static LazyLoadWithInnerClass getInstance(){
        return Inner.instance;
    }
}

優(yōu)點:

  1. 線程安全
  2. 懶加載

缺點:

  1. 實現(xiàn)復(fù)雜

適用:相對上一個雙重檢查谋旦,多一次(或多次)尋址開銷,不推薦使用

1.2.5 枚舉

通過枚舉實現(xiàn)單例屈尼,推薦使用此方式册着,能在多個維度保證安全:

  1. 線程安全
  2. 序列化不破壞唯一性
  3. 反射調(diào)用不破壞唯一性
  4. 實現(xiàn)簡單

實現(xiàn)如下:

enum LazyLoadWithEnum implements Singleton{
    INSTANCE;
    Singleton getInstance(){
        return INSTANCE;
    }
}

2. 其他創(chuàng)建對象方式對單例唯一的破壞

單例模式的核心是,在設(shè)定的上下文中脾歧,指定的類的實例僅有一個甲捏,此處的上下文,根據(jù)需求不同鞭执,可能指JVM司顿、同一SpringContext等,然而兄纺,我們都學(xué)過大溜,創(chuàng)建一個對象有4種方式:

  1. new關(guān)鍵字:new Object()
  2. 對象反序列化:objectInputStream.readObject()
  3. 反射調(diào)用:Object.class.getDeclaredConstructor().newInstance()
  4. clone方法:obj.clone()

雖然在上述的單例實現(xiàn)中,已經(jīng)考慮了構(gòu)造器私有化估脆,保證使用者無法通過new一個新對象的方式破壞唯一性钦奋,但仍舊有可能通過其他三種方式,獲取到另外的實例旁蔼,破壞單例模式的唯一性

2.1 clone方法另外創(chuàng)建單例對象破壞單例唯一性

clone方法為Object的方法锨苏,理論上所有的對象都繼承,但是由于此方法為protected方法棺聊,且要求必須顯式的implement Cloneable接口伞租,換句話說,必須本類(或父類)顯式實現(xiàn)clone方法并將之?dāng)U大為public權(quán)限限佩,因此葵诈,clone方法雖然會破壞單例模式的唯一性裸弦,但更多是由于在定義單例類時,override clone方法時造成的錯誤作喘,因此不做討論

2.2 對象反序列化破壞單例唯一性

對于非Enum的單例實現(xiàn)來說理疙,對象反序列化能破壞單例模式的唯一性:

private static void testSerialization(){
        Singleton created = LazyLoadWithInnerClass.getInstance();
        System.out.println(created.hashCode());
        File testFile = new File("obj.txt");
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(testFile));
            objectOutputStream.writeObject(created);
            objectOutputStream.close();

            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(testFile));
            Singleton dematerializedObject = (Singleton) objectInputStream.readObject();
            objectInputStream.close();
            System.out.println(dematerializedObject.hashCode());
            Assertions.assertTrue(dematerializedObject == created,"破壞了單例唯一性");
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            testFile.delete();
        }
    }

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

1995265320
1880587981
Exception in thread "main" java.lang.AssertionError: 破壞了單例唯一性
    at utils.Assertions.assertTrue(Assertions.java:27)
    at design.pattern.singleton.TestCase.testSerialization(TestCase.java:116)
    at design.pattern.singleton.TestCase.main(TestCase.java:25)

2.2.1 源碼解析

ObjectInputStream.readObject方法內(nèi)部,會判斷要反序列化的對象的類型泞坦,對于普通對象(非String, Class,* ObjectStreamClass, array, or enum constant),調(diào)用下列方法來反序列化:

 private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        // 略
        // ↓↓↓↓↓↓↓↓↓初始化新實例↓↓↓↓↓↓↓↓↓↓
        obj = desc.isInstantiable() ? desc.newInstance() : null;
        // 略
        // ↓↓↓↓↓↓↓↓↓調(diào)用readResolve方法覆蓋↓↓↓↓↓↓↓↓↓↓
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            // 略
            if (rep != obj) {
                // 略
                handles.setObject(passHandle, obj = rep);
            }
        }
        return obj;
    }

過程為:

  1. 對于可以實例化(調(diào)用public無參構(gòu)造器)的對象窖贤,調(diào)用ObjectStreamClass的newInstance方法:

    Object newInstance()
        throws InstantiationException, InvocationTargetException,
            UnsupportedOperationException
    {
        // 略
        return cons.newInstance();
        // 略
    }
    

    變量cons為private Constructor<?> cons;,忽略安全檢查部分,實際上通過反射來創(chuàng)建新的實例對象贰锁,如果將此新創(chuàng)建的對象作為最終結(jié)果赃梧,則破壞了單例的唯一性

  2. 如果目標(biāo)類實現(xiàn)了readResolve方法,則調(diào)用readResolve方法豌熄,并用返回的結(jié)果覆蓋上一步的結(jié)果授嘀,因此,一種避免序列化破壞單例唯一性的思路即手動實現(xiàn)readResolve方法:

    private Object readResolve(){
        return Inner.instance;
    }
    

2.3 反射調(diào)用破壞單例的唯一性

與上述反序列化的源碼解析類似锣险,直接通過class對象的newInstance方法或者通過獲取其Constructor對象并調(diào)用來創(chuàng)建實例時蹄皱,也會重新生成一個新的實例,從而破壞單例的唯一性芯肤,當(dāng)然巷折,通過在構(gòu)造器中維護(hù)一個flag變量,在多次構(gòu)造時拋出異常可以(一定程度上)避免此問題:

    private static boolean initFlag = false;
    private LazyLoadWithDoubleCheckSynchronization(){
        if (initFlag) {
            throw new RuntimeException("多次嘗試調(diào)用構(gòu)造函數(shù)纷妆,破壞單例的唯一性");
        }
        initFlag = true;
        // 其他構(gòu)造過程
    }

2.4 使用枚舉避免序列化和反射過程中對單例的破壞

使用枚舉來實現(xiàn)單例模式盔几,可以防止序列化和反射過程中對單例的破壞

2.4.1 單例模式避免序列化過程中對單例唯一性的破壞

對于單例的反序列化晴弃,在從流解析對象過程中掩幢,調(diào)用如下方法:

private Enum<?> readEnum(boolean unshared) throws IOException {
        // 略
        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            Enum<?> en = Enum.valueOf((Class)cl, name);
            result = en;
            // 略
        }
        return result;
    }

可見流存儲的僅為枚舉的name,反序列化時上鞠,根據(jù)name际邻,調(diào)用Enum的valueOf方法,獲取JVM已經(jīng)初始化的實例芍阎,因此世曾,單例模式使用枚舉實現(xiàn),可以保證反序列化不破壞單例的唯一性

2.4.2 單例模式避免反射破壞單例唯一性

枚舉類無法進(jìn)行反射調(diào)用谴咸,實際考慮使用下面的代碼嘗試進(jìn)行反射創(chuàng)建枚舉實例

    private static void testReflection()  {
        try {
            Singleton created = LazyLoadWithEnum.INSTANCE.getInstance();
            Constructor<LazyLoadWithEnum> constructor = LazyLoadWithEnum.class.getDeclaredConstructor(String.class, int.class);
            constructor.setAccessible(true);
            Singleton instanceWithReflection = constructor.newInstance();


            System.out.println(created.hashCode());
            System.out.println(instanceWithReflection.hashCode());
            Assertions.assertTrue(instanceWithReflection == created,"反射破壞單例唯一性");
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

注意轮听,枚舉類的Constructor對象獲取和newInstance方法均不同于普通類

  1. 枚舉類看似有空構(gòu)造方法,其實并非如此岭佳,下面是使用【DJ Java Decompiler 3.12】反編譯的枚舉對應(yīng)的class文件:

        final class LazyLoadWithEnum extends Enum
        implements Singleton
    {
    
        public static LazyLoadWithEnum[] values()
        {
            return (LazyLoadWithEnum[])$VALUES.clone();
        }
    
        public static LazyLoadWithEnum valueOf(String name)
        {
            return (LazyLoadWithEnum)Enum.valueOf(design/pattern/singleton/LazyLoadWithEnum, name);
        }
    
        private LazyLoadWithEnum(String s, int i)
        {
            super(s, i);
        }
    
        Singleton getInstance()
        {
            return INSTANCE;
        }
    
        public static final LazyLoadWithEnum INSTANCE;
        private static final LazyLoadWithEnum $VALUES[];
    
        static 
        {
            INSTANCE = new LazyLoadWithEnum("INSTANCE", 0);
            $VALUES = (new LazyLoadWithEnum[] {
                INSTANCE
            });
        }
    }
    

    觀察發(fā)現(xiàn)血巍,此類未定義無參構(gòu)造器,取而代之的是private LazyLoadWithEnum(String s, int i)珊随,因此獲取構(gòu)造器時述寡,應(yīng)指定參數(shù)列表為(String,int)

  2. 調(diào)用Constructor的newInstance()方法時柿隙,如果是枚舉類型,會拋出異常:

    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        // 略
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        // 略
    }

因此鲫凶,使用Enum來實現(xiàn)單例禀崖,可以保證不會因為反射調(diào)用來破壞單例的唯一性

3. 參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市螟炫,隨后出現(xiàn)的幾起案子波附,更是在濱河造成了極大的恐慌,老刑警劉巖昼钻,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叶雹,死亡現(xiàn)場離奇詭異,居然都是意外死亡换吧,警方通過查閱死者的電腦和手機(jī)折晦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沾瓦,“玉大人满着,你說我怎么就攤上這事」彷海” “怎么了风喇?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缕探。 經(jīng)常有香客問我魂莫,道長,這世上最難降的妖魔是什么爹耗? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任耙考,我火速辦了婚禮,結(jié)果婚禮上潭兽,老公的妹妹穿的比我還像新娘倦始。我一直安慰自己,他們只是感情好山卦,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布鞋邑。 她就那樣靜靜地躺著,像睡著了一般账蓉。 火紅的嫁衣襯著肌膚如雪枚碗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天铸本,我揣著相機(jī)與錄音肮雨,去河邊找鬼。 笑死归敬,一個胖子當(dāng)著我的面吹牛酷含,可吹牛的內(nèi)容都是我干的鄙早。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼椅亚,長吁一口氣:“原來是場噩夢啊……” “哼限番!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起呀舔,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤弥虐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后媚赖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霜瘪,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年惧磺,在試婚紗的時候發(fā)現(xiàn)自己被綠了颖对。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡磨隘,死狀恐怖缤底,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情番捂,我是刑警寧澤个唧,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站设预,受9級特大地震影響徙歼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鳖枕,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一魄梯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧耕魄,春花似錦画恰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缠局。三九已至则奥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狭园,已是汗流浹背读处。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留唱矛,地道東北人罚舱。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓井辜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親管闷。 傳聞我的和親對象是個殘疾皇子粥脚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

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