引言
序列化破壞單例:一個單例對象創建好后,有時候需要將對象序列化后寫入磁盤,下次使用時再從磁盤中讀取對象並進行反序列化,將其轉化為內存對象。反序列化后的對象將會重新分配內存,即重新創建。如果序列化的目標對象為單例對象,就違背了單例模式的初衷,相當於破壞了單例,看如下代碼。
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(); } }
運行結果如下圖
從運行結果可以看出,反序列化后的對象和手動創建的對象是不一致的,實例化了兩次,違背了單例模式的設計初衷。那么如何保證在序列化的情況下也能實現單例模式呢?只需要增加 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(); } }
運行結果如下:
枚舉類型其實通過類名和類對象找到一個唯一的枚舉對象。因此,枚舉對象不可能別類加載器加載多次。
那么反射能否破壞枚舉式單例模式呢?測試代碼如下:
@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); } } } }
容器式單例模式使用與實例非常多的情況,編輯管理。單它是非線程安全的。