單例模式-2(注冊式單例)


引言

  序列化破壞單例:一個單例對象創建好后,有時候需要將對象序列化后寫入磁盤,下次使用時再從磁盤中讀取對象並進行反序列化,將其轉化為內存對象。反序列化后的對象將會重新分配內存,即重新創建。如果序列化的目標對象為單例對象,就違背了單例模式的初衷,相當於破壞了單例,看如下代碼。

public class SeriableSingleton implements Serializable {

    public final static SeriableSingleton INSTANCE = new SeriableSingleton();

    private SeriableSingleton(){}
    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }
}

測試代碼如下

    @Test
    void SeriableSingletonTest(){
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
SeriableSingletonTest

運行結果如下圖

  從運行結果可以看出,反序列化后的對象和手動創建的對象是不一致的,實例化了兩次,違背了單例模式的設計初衷。那么如何保證在序列化的情況下也能實現單例模式呢?只需要增加 readResolve() 方法即可。

public class SeriableSingleton implements Serializable {

    public final static SeriableSingleton INSTANCE = new SeriableSingleton();

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

運行結果如下圖:

 

讀JDK 的 源碼發現,雖然增加 readResolve() 方法返回實例解決了單例模式被破壞的問題,但是實際上實例化了兩次,只不過新創建的對象沒有被返回而已。如果創建對象的動作發生頻率加快,就意味着內存分配的開銷也會隨之增大,這時候就注冊式單例就可以登場了。

注冊式單例模式

  注冊式單例模式又稱為等級式單例模式,就是將每一個實例都登記到某一個地方,使用唯一的標識獲取實例。注冊式單例i模式有兩種:一種是枚舉式單例模式,另一種是容器式單例模式。

1.枚舉式單例模式

  枚舉式單例模式寫法如下:

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;
    }
}

  測試代碼如下:

    @Test
    void EnumSingletonTest(){
        EnumSingleton e1 = null;
        EnumSingleton e2 = EnumSingleton.getInstance();
        e2.setData(new Object());
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(e2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            e1 = (EnumSingleton)ois.readObject();
            ois.close();
            System.out.println(e1.getData());
            System.out.println(e2.getData());
            System.out.println(e1.getData() == e2.getData());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
EnumSingletonTest

  運行結果如下:

  枚舉類型其實通過類名和類對象找到一個唯一的枚舉對象。因此,枚舉對象不可能別類加載器加載多次。

  那么反射能否破壞枚舉式單例模式呢?測試代碼如下:

    @Test
     void EnumSingletonTestThread() {
        try{
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor();
            c.newInstance();
        } catch (Exception e){
            e.printStackTrace();
        }
    }

  運行結果如下:

 報錯沒有找到無參構造方法。查看 Enum 源碼  他的構造方法只有一個 protected 類型的構造方法,代碼如下:

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

  再做如下測試:

    @Test
     void EnumSingletonTestThread() {
        try{
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
            c.setAccessible(true);
            EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("l-coil",666);
        } catch (Exception e){
            e.printStackTrace();
        }
    }

   測試結果如下:

  

  報錯已經很明顯了,不能用反射來創建枚舉類型。

  枚舉式單例模式也是 Effective Java 書中推薦的一種單例模式實現寫法。JDK 枚舉的語法特殊性質及繁殖也為枚舉報價護航,讓枚舉式單例模式成為一種比較優雅的實現。 

2.容器式單例

  容器式單例寫法如下:

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 obj = null;
                try{
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            } else {
              return ioc.get(className);
            }
        }
    }
}

  容器式單例模式使用與實例非常多的情況,編輯管理。單它是非線程安全的。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM