這怕是最全的【單例模式】,可以拉著面試官掰扯半小時

前言

歡迎最美最帥的你點贊哦~~~!

單例模式是面向?qū)ο蟮木幊陶Z言23種設(shè)計模式之一,屬于創(chuàng)建型設(shè)計模式。主要用于解決對象的頻繁創(chuàng)建與銷毀問題,因為單例模式保證一個類僅會有一個實例。大部分對單例模式應(yīng)該都知道一些,但面試的時候可能回答不會很完整,不能給自己加分,甚至扣分。

單一的知識點并不能對自己在面試的時候帶來加分,而系統(tǒng)的只是則會讓面試官另眼相看,而本文會系統(tǒng)的介紹單例模式的基礎(chǔ)版本與完美版本,基本上將單例模式的內(nèi)容完全包括。如果認為有不同的意見可以留言交流。

源碼已收錄github 查看源碼

單例模式最重要的就是保證一個類只會出現(xiàn)一個實例,那么超過一個就不能被稱為是單例,所有其代碼構(gòu)成如下特點。

  1. 私有化構(gòu)造器,禁止從外部創(chuàng)建單例對象。

  2. 提供一個全局的訪問點獲取單例對象。

什么是全局訪問點? 好吧,上面的話語太文鄒鄒了,如果我說公共的靜態(tài)方法呢?

餓漢、懶漢

主要分為餓漢模式和懶漢模式。那何為餓漢?何為懶漢?

小麗的爸爸從小生活很艱苦,經(jīng)歷了饑荒年代,所以對食物非常緊張。當(dāng)小麗去上學(xué)的時候,不管小麗是否需要,都會給小麗準備很多的零食。

而小明的爸爸則是一個非常懶惰的人,所有的事情都會到最后才去做,所有事情只有當(dāng)有別人來叫他的時候,他才會把事情做完

這樣就引出了我們對餓漢模式和懶漢模式的定義:

餓漢模式:不管單例對象是否被使用,都會先創(chuàng)建出一個對象。餓漢模式存在資源浪費的問題,因為很有可能對象創(chuàng)建出來只會永遠都不會被使用到。

代碼如下:


package demo.single;

/**

* 餓漢模式

*/

public class HungrySingle {

    /**

    * 餓漢模式,不管hungrySingle對象是否有使用到,都會先創(chuàng)建出來

    * 由于餓漢模式在對象使用之前就已經(jīng)被創(chuàng)建,所以是不會存在線程安全問題

    */

    private static HungrySingle hungrySingle = new HungrySingle();

    /**

    * 私有化構(gòu)造器,禁止外部創(chuàng)建

    */

    private HungrySingle(){

    }

    /**

    * 提供獲取實例的方法

    */

    public static HungrySingle getInstance(){

        return hungrySingle;

    }

}

懶漢模式:不會先將對象創(chuàng)建出來,而是等到有人使用的時候才會創(chuàng)建。相比餓漢模式,懶漢模式不會存在資源浪費的情況,所以基本都會選擇懶漢模式。

代碼如下:


package demo.single;

/**

* 懶漢模式

*/

public class LazySingle {

    /**

    * 懶漢模式,不會先創(chuàng)建對象,而是在調(diào)用的時候才會創(chuàng)建對象

    */

    private static LazySingle lazySingle = null;

    private LazySingle() {

    }

    /**

    * 調(diào)用的時候創(chuàng)建對象并返回

    */

    public static LazySingle getInstance(){

        if(lazySingle == null){

            lazySingle = new LazySingle();

        }

        return lazySingle;

    }

}

小李:面試官,您看我這樣的解釋可還行。

面試官:單線程下是挺好的,如果在多線程環(huán)境下呢?

小李:這個我知道,加鎖?。?/p>

面試官:出門左轉(zhuǎn)電梯直達!

其實加鎖也沒答錯,關(guān)鍵問題在于如何加鎖!

直接將獲取實例的方法內(nèi)容寫入同步代碼塊中,解決了多線程安全的問題,但是并發(fā)效率的問題又暴露了出來。你想啊,現(xiàn)在鎖住了這方法,而無論單例的對象是否創(chuàng)建,都會經(jīng)過獲取鎖、釋放鎖的過程。這樣的性能顯然是不能接受的。

小李:我想想啊~~~! Emmmmm...! 有了,我們可以在同步代碼塊外層加一個判斷,如果對象已經(jīng)創(chuàng)建則直接返回。

面試官:這樣解決了一部分的并發(fā)效率問題,但是如果在創(chuàng)建的時候同時有很多的線程訪問,是不是也會有并發(fā)的效率問題呢?再優(yōu)化優(yōu)化。

小李一想,確實是這樣,如果對象還沒有創(chuàng)建出來的時候,就有很多的線程來訪問,也會出現(xiàn)問題,假設(shè)有兩個線程同時訪問,當(dāng)A線程優(yōu)先爭搶到鎖,A進入同步代碼塊執(zhí)行,此時B沒有爭搶到鎖,將處于等待狀態(tài),而當(dāng)A線程執(zhí)行完成后釋放鎖,B進入同步代碼塊執(zhí)行,此時B線程同樣會創(chuàng)建出一個對象,破壞了單例。

小李:面試官,我明白了,可以在同步代碼塊中再加一層if判斷,如果對象已經(jīng)創(chuàng)建,就直接返回即可。

Double Check

上面最后的結(jié)果就是我們常說的Double Check,即雙重鎖檢查。雙重鎖檢查在很多地方都被運用到,代碼如下。


package demo.single;

/**

* 懶漢模式

*/

public class LazySingle {

    /**

    * 懶漢模式,不會先創(chuàng)建對象,而是在調(diào)用的時候才會創(chuàng)建對象

    */

    private static LazySingle lazySingle = null;

    private LazySingle() {

    }

    /**

    * 調(diào)用的時候創(chuàng)建對象并返回

    */

    public static LazySingle getInstance(){

        //first check

        if(lazySingle != null){

            synchronized (LazySingle.class){

                //double check

                if(lazySingle == null){

                    lazySingle = new LazySingle();

                }

            }

        }

        return lazySingle;

    }

}

面試官:小李,你多線程運行一下代碼看看呢。

小李:好勒! 好像挺正常啊。等等, 好像不對, 這里還是出現(xiàn)了多個對象!!!啊~~,這是為什么啊,我都懵了,這完全超出了我的能力范圍。

面試官:哈哈,小子,這下知道誰是大佬了吧?我來給你好好解釋一下,其實,這和我們的代碼沒有關(guān)系,正常來講,應(yīng)該不會出現(xiàn)這樣的問題,但是我們都知道,代碼在運行過程中,會被編譯成一條一條的指令運行,而JVM在運行時,在保證單線程最終結(jié)果不會受影響的情況下,對指令進行優(yōu)化,就有可能對指令進行重排序,同樣會破壞單例。


lazySingle = new LazySingle();

//這樣一段代碼在運行時會生成3條指令,即: 1. 分配內(nèi)存空間 2. 創(chuàng)建對象 3. 指向引用

//正常情況下是會按照1 2 3順序執(zhí)行,但JVM優(yōu)化器進行指令重排后,則可能變?yōu)椋?. 分配內(nèi)存空間 3. 指向引用  2. 創(chuàng)建對象

//在單線程下,這樣的優(yōu)化沒有問題,但是多線程下,線程是在爭搶CPU時間碎片的。假設(shè)A剛剛執(zhí)行完 1 3 //條指令,此時B爭搶到時間碎片,發(fā)現(xiàn)對象不為空了,就直接返回,但此時對象還沒有真正被創(chuàng)建。B調(diào)用

//此對象就會拋出異常

//而volatile關(guān)鍵字修飾的變量可以禁止指令重排序,則可以保證指令會是1 2 3順序執(zhí)行。

//加上volatile修飾

private volatile  static LazySingle lazySingle = null;

小李: 終于解決了,好難啊,一個簡單的單例模式居然有這么多的細節(jié)。

面試官:你以為這就完了?

內(nèi)部類的單例

使用內(nèi)部類的方式可以非常完美的完成單例模式,而實現(xiàn)代碼也非常簡單。


package demo.single;

/**

* 內(nèi)部類的方式實現(xiàn)單例

*/

public class InnerSingle {

    /**

    * 私有化構(gòu)造器

    */

    private InnerSingle(){

    }

    /**

    * 私有內(nèi)部類

    */

    private static class Inner{

        //Jingtai內(nèi)部類持有外部類的對象

        public static final InnerSingle SINGLE = new InnerSingle();

    }

    /**

    * 返回靜態(tài)內(nèi)部類持有的對象

    */

    public static InnerSingle getInstance(){

        return Inner.SINGLE;

    }

}

可以看到,代碼中并沒有出現(xiàn)同步方法或者同步代碼塊,那么靜態(tài)內(nèi)部類的方式是如何做到安全的單例模式呢?

  1. 外部類加載的時候,不會立即加載內(nèi)部類,而是在調(diào)用的時候會加載內(nèi)部類。

  2. 不管多少線程訪問,JVM一定會保證類被正確的初始化,即靜態(tài)內(nèi)部類的方式是在JVM層面保證了線程安全

當(dāng)然,這樣也有一些缺點,那就是在創(chuàng)建單例對象的時候,如果需要傳參,那么靜態(tài)內(nèi)部類的方式會非常麻煩。

破壞單例

那么,上面的單例已經(jīng)完美了嗎?并沒有,看我如何將單例給破壞掉。

反射破壞

反射可以繞過私有構(gòu)造器的限制,創(chuàng)建對象。當(dāng)然正常的調(diào)用是不會發(fā)生單例被破壞的情況,但是如果偏偏有人不走尋常路呢,比如下面的調(diào)用。


package demo.single;

import java.lang.reflect.Constructor;

/**

* 反射破壞單例

*/

public class RefBreakSingleTest {

    public static void main(String[] args) throws Exception {

        //獲取類對象

        Class<LazySingle> lazySingleClass = LazySingle.class;

        //獲取構(gòu)造器

        Constructor<LazySingle> constructor = lazySingleClass.getDeclaredConstructor(null);

        constructor.setAccessible(true);

        //創(chuàng)建對象

        LazySingle lazySingle = constructor.newInstance(null);

        System.out.println(lazySingle);

        System.out.println(LazySingle.getInstance());

        System.out.println(lazySingle == LazySingle.getInstance());

    }

}

image

很明顯看到出現(xiàn)了兩個不同的兌現(xiàn),顯然,單例被破壞了!

對于這樣的情況該如何禁止呢?在網(wǎng)上查閱了很多資料,大部分是使用變量控制法,即在類中添加一個變量用于判斷單例類的構(gòu)造器是否有被調(diào)用,代碼如下。


    //添加變量控制,防止反射破壞

    private static boolean isInstance = false;

    private volatile  static LazySingle lazySingle = null;

    private LazySingle() throws Exception {

        if(isInstance){

            throw new Exception("the Constructor has be used");

        }

        isInstance = true;

    }

再次調(diào)用測試代碼,發(fā)現(xiàn)不能再創(chuàng)建多個單例對象,程序拋出了異常。

image

但是別忘了,屬性也是可以通過反射修改的(count、instance的判斷反射都能繞過)。


public class RefBreakSingleTest {

    public static void main(String[] args) throws Exception {

        //獲取類對象

        Class<LazySingle> lazySingleClass = LazySingle.class;

        //獲取構(gòu)造器

        Constructor<LazySingle> constructor = lazySingleClass.getDeclaredConstructor(null);

        constructor.setAccessible(true);

        //創(chuàng)建對象

        LazySingle lazySingle = constructor.newInstance(null);

        System.out.println(lazySingle);

        Field isInstance = lazySingleClass.getDeclaredField("isInstance");

        isInstance.setAccessible(true);

        isInstance.set(null,false);

        System.out.println(LazySingle.getInstance());

        System.out.println(lazySingle == LazySingle.getInstance());

    }

}

image

單例再次被破壞,感覺是不是已經(jīng)快崩潰了,一個單例咋這么多事呢??!既然私有屬性、私有方法在外部都能通過反射獲取,那有沒有反射不能獲取的呢?我在網(wǎng)上也找到了另外一種寫法,即私有內(nèi)部類的來持有實例控制變量,而我也通過測試,發(fā)現(xiàn)反射同樣能夠繞過從而破壞單例。


package demo.pattren.single;

import java.lang.reflect.Constructor;

import java.lang.reflect.Method;

public class BreakInnerTest {

    public static void main(String[] args) throws Exception {

        Class<LazySingle> lazySingleClass = LazySingle.class;

//        //獲取構(gòu)造器

        Constructor<LazySingle> constructor = lazySingleClass.getDeclaredConstructor(null);

        constructor.setAccessible(true);

        //創(chuàng)建對象

        LazySingle lazySingle = constructor.newInstance(null);

        //獲取內(nèi)部類的類對象

        Class<?> aClass = Class.forName("demo.pattren.single.LazySingle$InnerClass");

        Method[] methods = aClass.getMethods();

        Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();

        System.out.println(declaredConstructors);

        Constructor<?> declaredConstructor = declaredConstructors[0];

        declaredConstructor.setAccessible(true);

        //創(chuàng)建內(nèi)部類需要傳入一個外部類的對象

        Object o = declaredConstructor.newInstance(lazySingle);

        //成功繞過

        methods[0].invoke(o);

    }

}

目前網(wǎng)上基本都是這兩種,但是反射都是能夠繞過判斷進行破壞。可以這樣認為,這種方式反射是可以破壞的,不能100%保證單例不被破壞。歡迎各位提供完美的示例。

序列化破壞

Java的IO提供了對象流,用來將對象寫入磁盤、從磁盤讀取對象的功能。這也成為了單例的破壞點。


    public static void main(String[] args) throws Exception {

        //正常的方式獲取單例對象

        InnerSingle instance = InnerSingle.getInstance();

        //寫入磁盤

        FileOutputStream fos = new FileOutputStream("d:/single");

        ObjectOutputStream oos = new ObjectOutputStream(fos);

        oos.writeObject(instance);

        oos.close();

        fos.close();

        //從磁盤讀取對象

        FileInputStream fis = new FileInputStream("d:/single");

        ObjectInputStream ois = new ObjectInputStream(fis);

        InnerSingle innerSingle = (InnerSingle) ois.readObject();

        System.out.println(instance);

        System.out.println(innerSingle);

        System.out.println(innerSingle == instance);

    }

image

而序列化的方式JVM提供了一種機制,可以防止單例被破壞,即在單例類中添加readResovle方法。


//在反序列化時,readResolve方法,則直接返回該方法指定的對象

    private  Object readResolve(){

        return getInstance();

    }

測試結(jié)果:

image

序列化沒有再破壞單例,而這一切JDK是如何處理的呢?


public final Object readObject()

        throws IOException, ClassNotFoundException

    {

        if (enableOverride) {

            return readObjectOverride();

        }

        int outerHandle = passHandle;

        try {

        //關(guān)鍵代碼,最終返回的是此方法返回的對象

            Object obj = readObject0(false);

            handles.markDependency(outerHandle, passHandle);

            ClassNotFoundException ex = handles.lookupException(passHandle);

//more code but not importent

繼續(xù)深入,發(fā)現(xiàn)readObject0方法的關(guān)鍵代碼如下


byte tc;

        //取出文件的一個字節(jié),判斷讀取的對象類型

        while ((tc = bin.peekByte()) == TC_RESET) {

            bin.readByte();

            handleReset();

        }

        depth++;

        totalObjectRefs++;

        try {

            switch (tc) {

                case TC_NULL:

                    return readNull();

                case TC_ENUM:

                    return checkResolve(readEnum(unshared));

//判斷為對象類

                case TC_OBJECT:

                    return checkResolve(readOrdinaryObject(unshared));

                //more othrer case

繼續(xù)追蹤readOrdinaryObject方法,發(fā)現(xiàn)readReslove的關(guān)鍵代碼


//判斷是否有readReslove方法(desc.hasReadResolveMethod())

if (obj != null &&

            handles.lookupException(passHandle) == null &&

            desc.hasReadResolveMethod())

        {

          //執(zhí)行readReslove

            Object rep = desc.invokeReadResolve(obj);

            if (unshared && rep.getClass().isArray()) {

                rep = cloneArray(rep);

            }

            if (rep != obj) {

                // Filter the replacement object

                if (rep != null) {

                    if (rep.getClass().isArray()) {

                        filterCheck(rep.getClass(), Array.getLength(rep));

                    } else {

                        filterCheck(rep.getClass(), -1);

                    }

                }

                //最終返回readReslove方法的執(zhí)行結(jié)果

                handles.setObject(passHandle, obj = rep);

            }

        }

        return obj;

枚舉單例 - 最完美的單例模式

大神Josh Bloch在《Effective Java》中極力推薦使用枚舉的方式來實現(xiàn)單例。


package demo.single;

public enum EnumSingle {

    SINGLE;

    public void doJob(){

        System.out.println("doJob");

    }

}

枚舉類型是單例模式的最佳選擇,主要得益于JVM對于枚舉類型的支持:

  1. JVM保證枚舉類型的每個實例僅存在一份

  2. 枚舉類型的序列化與反序列化不會破壞其單例的特性(上面的源碼大家可以去找一找)

  3. 反射也不能破壞枚舉單例

可以說,枚舉天然就是單例的,那么你會選擇枚舉作為單例嗎?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容