架構師內功心法,經典高頻面試的單例模式詳解

1、單例模式應用場景

單例模式(Single Pattern)是指確保一個類在任何狀況下絕對只是一個實例,並提供一個全局的訪問點。 單例模式在現實生活中的應用也很普遍。例如國家總統、公司CEO、部門經理等。在java標準中,ServletContext、ServletContextConfig等;在Spring框架中ApplicationCotext;數據庫對應的鏈接池也都是單例形勢的。java

2、單例模式分類

2.1 餓漢式單例

餓漢式單例是在類加載的時候就當即初始化了,而且建立了單例對象。絕對的線程安全,在線程還沒出現之前就實例化了,不可能存在訪問安全問題。數據庫

優勢:沒有加任何的鎖,執行效率高,在用戶體驗上,比懶漢式更好。 缺點:類加載的時候就初始化了,無論用與不用都佔空間,浪費了內存,有可能佔着茅坑不拉屎。緩存

Spring中的IOC容器ApplicationContext自己就是典型的餓漢式單例。案例代碼:安全

public class HungrySingleton {

    /**
     * 先靜態後動態
     * 先屬性後方法
     * 先上後下
     */
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

還有一種寫法,是利用靜態代碼塊機制:app

public class HungrySingleton {
    
    private static final HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }

    private HungrySingleton() {}

    private HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

這兩種寫法都很簡單,也很容易理解。餓漢式使用在單例對象較少的狀況下。 下面來看下性能更優的寫法。框架

2.2 懶漢式單例

懶漢式單例是指被外部調用的時候纔會進行加載。示例代碼:ide

public class LazySingleton {

    /**
     * 懶漢式單例
     * 在外部須要使用的時候才進行實例化
     */
    private LazySingleton() {}

    private static LazySingleton lazySingleton = null;

    public static LazySingleton getInstance() {
        if(lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

編寫一個線程類ExectorThread:工具

public class ExectorThread implements Runnable {
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + lazySingleton);
    }

    public static void main(String[] args) {
        System.out.println("--------begin-------");
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("--------end---------");
    }
}

查看main方法屢次運行的結果發現:源碼分析

有必定概率會出現建立兩個不一樣結果的狀況,意味着上門的單例建立代碼存在線程安全隱患。咱們經過對代碼進行debug調試,發現經過不斷切換線程,並觀測其內存狀態,發如今線程環境下LazySingleton被實例化了兩次。有時候咱們獲得的運行結果多是相同的兩個對象,其實是被後面的執行線程給覆蓋了,看到了一個假象,線程安全隱患依然存在。這樣咱們須要在線程安全的環境下運行懶漢單例代碼。給getIntance()方法加上Synchronized關鍵字,使這個方法變成線程同步方法:性能

public class LazySingleton {

    /**
     * 懶漢式單例
     * 在外部須要使用的時候才進行實例化
     */
    private LazySingleton() {}

    private static LazySingleton lazySingleton = null;

    public synchronized static LazySingleton getInstance() {
        if(lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

添加synchronized關鍵字使用鎖,在線程數量比較多的狀況下,若是CPU分配壓力上升,會致使大批線程出現阻塞,從而致使程序運行性能大幅度降低。那麼,有木有一種更好的方式,既兼顧線程的安全性又提高程序性能呢?答案是確定的。咱們會使用雙重檢查鎖的單例模式:

public class LazyDoubleCheckSingletion {

    private volatile static LazyDoubleCheckSingletion lazy = null;

    private LazyDoubleCheckSingletion() {}

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

當第一個線程調用 getInstance()方法時,第二個線程也能夠調用getInstance()。當第一個線程執行到 synchronized 時會上鎖,第二個線程就會變成 MONITOR狀態,出現阻塞。此時,阻塞並非基於整個 LazySingleton 類的阻塞,而是在 getInstance()方法內部阻塞,只要邏輯不是太複雜,對於調用者而言感知不到。

可是,用到 synchronized關鍵字,總歸是要上鎖,對程序性能仍是存在必定影響的。難道就真的沒有更好的方案嗎?固然是有的。咱們能夠從類初始化角度來考慮,看下面的代碼,採用靜態內部類的方式:

public class LazyInnerClassSingleton {

    /**
     * 這種形式兼顧餓漢式的內存浪費,也兼顧 synchronized 性能問題
     * 完美地屏蔽了這兩個缺點
     */
    //若是沒使用的話,內部類是不加載的
    private LazyInnerClassSingleton() {}

    /**
     * 每個關鍵字都不是多餘的
     * static 是爲了使單例的空間共享
     * 保證這個方法不會被重寫,重載
     * @return
     */
    public static final LazyInnerClassSingleton getInstance() {
        //在返回結果之前,必定會先加載內部類
        return LazyHolder.LAZY;
    }

    //默認不加載
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY =
                new LazyInnerClassSingleton();
    }
}

這種形式兼顧餓漢式的內存浪費,也兼顧synchronized性能問題。內部類必定是要在方法調用以前初始化,巧妙地避免了線程安全問題。

  • 反射破壞單例

上面一些介紹單例模式的構造方法除了加上private之外,沒有作任何處理。若是使用反射來調用其構造方法,而後再調用getInstance()方法,應該就會有兩個不一樣的實例。仍是以LazyInnerClassSingleton爲例:

public static void main(String[] args) {
        Class<?> clazz = LazyInnerClassSingleton.class;
        try {
            //經過反射機制拿到私有的構造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //強制訪問
            c.setAccessible(true);
            ////暴力初始化
            Object o1 = c.newInstance();
            //調用了兩次構造方法,至關於 new 了兩次
            Object o2 = c.newInstance();

            System.out.println(o1 == o2);

        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

運行結果是:

運行結果很顯然是建立了兩個不一樣的實例。如今咱們對其構造方法作一些限制,一旦出現重複建立實例,則直接拋出異常。來看優化後的代碼:

/**
     * 這種形式兼顧餓漢式的內存浪費,也兼顧 synchronized 性能問題
     * 完美地屏蔽了這兩個缺點
     */
    //若是沒使用的話,內部類是不加載的
    private LazyInnerClassSingleton() {
        if(LazyHolder.LAZY != null) {
            throw new RuntimeException("Multiple instances are not allowed to be created!");
        }
    }

    /**
     * 每個關鍵字都不是多餘的
     * static 是爲了使單例的空間共享
     * 保證這個方法不會被重寫,重載
     * @return
     */
    public static final LazyInnerClassSingleton getInstance() {
        //在返回結果之前,必定會先加載內部類
        return LazyHolder.LAZY;
    }

    //默認不加載
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY =
                new LazyInnerClassSingleton();
    }

再次運行結果:

至此,史上最牛 B 的單例寫法便大功告成。

  • 序列化破壞單例

當咱們將一個單例對象建立好後,有時候須要將對象序列化後寫入到磁盤,下次使用的時候再從磁盤中讀取到對象,反序列化爲內存對象。反序列化後的對象會從新分配內存,即從新建立。那麼若是序列化的目標的對象爲單例對象,就違背了單例模式的初衷,至關於破壞了單例,來看一下代碼:

public class SerializeSingleton implements Serializable {

    public static final SerializeSingleton INSTANCE = new SerializeSingleton();

    private SerializeSingleton() {}

    public static SerializeSingleton getInstance() {
        return INSTANCE;
    }
    
}

編寫測試代碼:

public static void main(String[] args) {

        SerializeSingleton s1 = null;
        SerializeSingleton s2 = SerializeSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SerializeSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SerializeSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SerializeSingleton)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
}

運行main方法結果:

運行結果能夠看出反序列化後的對象和手動建立的對象不一致,實例化了兩次,違背了單例的設計初衷。那麼,如何保證序列化的狀況下也可以實現單例呢? 其實很簡單,只須要增長readResolve() 方法便可。來看一下優化後的代碼:

public class SerializeSingleton implements Serializable {

    public static final SerializeSingleton INSTANCE = new SerializeSingleton();

    private SerializeSingleton() {}

    public static SerializeSingleton getInstance() {
        return INSTANCE;
    }
    
    private Object readResolve() {
        return INSTANCE;
    }
    
}

再次運行結果:

爲何要這麼寫呢?咱們來一塊兒看下JDK的源碼實現吧,進入ObjectInputStream類的readObject()方法,代碼以下:

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
}

咱們發如今readObject()方法中又調用了咱們重寫的readObject0()方法。進入readObject0()方法,代碼以下:

private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
}

咱們在源碼中看到了TC_OBJECT中判斷,調用了readOrdinaryObject()方法,繼續看源碼:

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            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);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
}

發現調用了 ObjectStreamClass 的 isInstantiable()方法,而isInstantiable()裏面的代碼以下:

boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
}

代碼看起來很簡單,就是判斷了如下構造方法是否爲空,構造方法不爲空就返回true。這樣意味着,只要有無參構造方法就會實例化。

這個時候,其實仍是沒找到爲何加上readResolve()方法就避免了單例被破壞的真正緣由。再回到ObjectInputStream的readOrdinaryObject()方法繼續往下看:

判斷無參構造方法是否存在以後,又調用了hasReadResolveMethod()方法,來看代碼:

boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
}

邏輯很是簡單,就是判斷 readResolveMethod 是否爲空,不爲空就返回 true。那麼 readResolveMethod 是在哪裏賦值的呢?經過全局查找找到了賦值代碼在私有方法 ObjectStreamClass()方法中給 readResolveMethod 進行賦值,來看代碼:

readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);

上面的邏輯其實就是經過反射找到一個無參的 readResolve()方法,而且保存下來。如今再回到 ObjectInputStream 的 readOrdinaryObject() 方法繼續往下看,若是readResolve()存在則調用 invokeReadResolve()方法,來看代碼:

Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (readResolveMethod != null) {
            try {
                return readResolveMethod.invoke(obj, (Object[]) null);
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof ObjectStreamException) {
                    throw (ObjectStreamException) th;
                } else {
                    throwMiscException(th);
                    throw new InternalError(th);  // never reached
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
}

能夠看到在invokeReadResolve()方法中用反射調用了readResolveMethod方法。 經過 JDK 源碼分析咱們能夠看出,雖然,增長readResolve()方法返回實例,解決了單例被破壞的問題。可是,咱們經過分析源碼以及調試,咱們能夠看到實際上實例化了兩次,只不過新建立的對象沒有被返回而已。那若是,建立對象的動做發生頻率增大,就意味着內存分配開銷也就隨之增大,難道真的就沒辦法從根本上解決問題嗎?下面咱們來註冊式單例也許能幫助到你。

2.3 註冊式單例

註冊式單例又稱登記式單例,就是將每個實例都登記到一個地方,使用惟一標識獲取實例。註冊的方式有兩種:一種爲容器緩存,一種爲枚舉登記。先來看下枚舉式單例的寫法,建立EnumSingleton類:

public enum EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

編寫測試main方法;

public static void main(String[] args) {
        try {
            EnumSingleton instance1 = null;
            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setData(new Object());
            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance1 = (EnumSingleton) ois.readObject();
            ois.close();
            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData() == instance2.getData());
        }catch (Exception e){
            e.printStackTrace();
        }
}

運行結果是:

沒有作任何的處理,咱們發現運行的結果和咱們預期的同樣。那麼枚舉式單例如此神奇,它的神祕之處體如今哪呢?下面咱們就經過分析源碼來揭開它的神祕面紗。 咱們使用jad反編譯工具(https://varaneckas.com/jad/) 生成的EnumSingleton.jad文件,打開這個文件發現這一段代碼:

static
{
    INSTANCE = new EnumSingleton("INSTANCE", 0);
    $VALUES = (new EnumSingleton[] {
    INSTANCE
    });
}

發現枚舉單例在靜態模塊中就給INSTANCE進行了賦值,是餓漢式單例的實現。 咱們回想序列化可否破壞枚舉式單例呢?再回到以前的源碼ObjectInputStream的readObject0()方法:

private Object readObject0(boolean unshared) throws IOException {
...
    case TC_ENUM:
        return checkResolve(readEnum(unshared));
...

咱們看到在 readObject0()中調用了 readEnum()方法,來看readEnum()中代碼實現:

private Enum<?> readEnum(boolean unshared) throws IOException {
        if (bin.readByte() != TC_ENUM) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: " + desc);
        }

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
}

發現枚舉類型實際上是經過類名和class對象找到一個惟一的枚舉對象。 所以,枚舉對象不可能被加載器加載屢次。那麼反射能破壞枚舉式單例嗎?來看一下測試代碼:

public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor();
            c.newInstance();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
}

運行結果:

報的是 java.lang.NoSuchMethodException 異常,意思是沒找到無參的構造方法。咱們打開 java.lang.Enum 的源碼代碼,查看它的構造方法,只有一個 protected的構造方法,代碼以下:

protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
}

那咱們再作這樣一個測試:

public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
            c.setAccessible(true);
            EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("Kevin", 123);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
}

運行結果是:

這時錯誤已經很是明顯了,告訴咱們 Cannot reflectively create enum objects,不能用反射來建立枚舉類型。仍是看下JDK源碼,看下Constructor類的newInstance()方法:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
}

在newInstance()方法中作了強制性的判斷,若是修飾符Modifier.ENUM枚舉類型,直接拋出異常。到此爲止,咱們應該很是清晰明瞭了。

枚舉類型單例也是《Effective Java》書中很是推薦的一種單例的實現寫法。在 JDK 枚舉的語法特殊性,以及反射也爲枚舉保駕護航,讓枚舉式單例成爲一種比較優雅的實現。

註冊式單例還有另一種寫法,容器緩存的寫法,建立ContainerSingleton類:

public class ContainerSingleton {

    private ContainerSingleton() {}

    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getBean(String className) {
        synchronized (ioc) {
            if(!ioc.containsKey(className)) {
                Object object = null;
                try {
                    object = Class.forName(className).newInstance();
                    ioc.put(className, object);
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                return object;
            }else {
                return ioc.get(className);
            }
        }
    }
}

容器式寫法適用於建立實例很是多的狀況,便於管理。可是,是非線程安全的。到此,註冊式單例介紹完畢。

來看看 Spring 中的容器式單例 的實現代碼:

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
    /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */
    private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>(16);
}

2.4 ThreadLocal 線程單例

ThreadLocal不能保證其建立的對象是全局惟一的,可是能保證在單個線程中是惟一的,天生的線程安全。 下面來看下示例代碼:

public class ThreadLocalSingleton {

    private static final ThreadLocal<ThreadLocalSingleton> instance = new
            ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton() {}

    public static ThreadLocalSingleton getInstance() {
        return instance.get();
    }
}

寫一下測試代碼:

public static void main(String[] args) {
        System.out.println("begin");
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("end");
}

運行結果:

在主線程 main 中不管調用多少次,獲取到的實例都是同一個,都在兩個子線程中分別獲取到了不一樣的實例。那麼ThreadLocal是若是實現這樣的效果的呢?咱們知道上面的單例模式爲了達到線程安全的目的,給方法上鎖,以時間換空間。 ThreadLocal將全部的對象所有放在ThreadLocalMap中,爲每一個線程都提供一個對象,其實是以空間換時間來實現線程間隔離的。

3、單例模式總結

單例模式能夠保證內存裏只有一個實例,減小了內存開銷;能夠避免對資源的多重佔用。單例模式看起來很是簡單,實現起來其實也很是簡單。

相關文章
相關標籤/搜索