使用枚舉來實現單例模式

單例模式的實現方式有很多種,詳情可以參考單例模式的7種實現方式及分析,從線程安全以及懶加載等角度來看其中第6種(double check)和第7種(靜態內部類)的實現方式都是值得推薦並且應用廣泛的,但是它們(包括第1到第7種)都有一個痛點,就是無法阻止通過反射或者序列化來破解單例對象的唯一性

反射破解

下列代碼以double check方式實現的單例模式爲示例,詳情如下:

  • 代碼
public class SyncDoubleCheckLazy {
    private SyncDoubleCheckLazy() {

    }

    private static SyncDoubleCheckLazy singleton = null;

    public static SyncDoubleCheckLazy getSingleton() {
        if (singleton == null) {
            synchronized (SyncDoubleCheckLazy.class) {
                if (singleton == null) {
                    singleton = new SyncDoubleCheckLazy();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //通過getSingleton方法獲取的單例對象
        SyncDoubleCheckLazy originalSingleton = SyncDoubleCheckLazy.getSingleton();
        //通過反射創建的非單例對象
        //這裏插播一條小知識,getConstructor無法獲取到私有的構造方法,因爲getConstructor只返回聲明爲public的構造方法,
        //但是getDeclaredConstructor可以,這個方法會返回所有構造器,包括public的和非public的;
        //這個規則同樣適用於其它反射操作;
        SyncDoubleCheckLazy newSingleton = SyncDoubleCheckLazy.class.getDeclaredConstructor().newInstance();
        //打印結果看看這兩個對象是否是一個
        System.out.println(originalSingleton == newSingleton);
    }
}
  • 打印結果
false

序列化破解

下列代碼以double check方式實現的單例模式爲示例,詳情如下:

  • 代碼
public class SyncDoubleCheckLazy implements Serializable {
    private SyncDoubleCheckLazy() {

    }

    private static SyncDoubleCheckLazy singleton = null;

    public static SyncDoubleCheckLazy getSingleton() {
        if (singleton == null) {
            synchronized (SyncDoubleCheckLazy.class) {
                if (singleton == null) {
                    singleton = new SyncDoubleCheckLazy();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //通過getSingleton方法獲取的單例對象
        SyncDoubleCheckLazy originalSingleton = SyncDoubleCheckLazy.getSingleton();
        //通過序列化創建的非單例對象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("newSingleton"));
        oos.writeObject(originalSingleton);
        oos.flush();
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("newSingleton"));
        SyncDoubleCheckLazy newSingleton = (SyncDoubleCheckLazy) ois.readObject();
        ois.close();

        //打印結果看看這兩個對象是否是一個
        System.out.println(originalSingleton == newSingleton);
    }
}
  • 打印結果
false

經過前面的鋪墊之後,我們接下來就進入我們的正題了,即枚舉單例模式以及它是如何避免反射和序列化來破解單例模式的;

枚舉單例代碼實現

public enum EnumSingleton {
    INSTANCE;
}

反射破解

  • 代碼
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //直接獲取原單例對象
        EnumSingleton originalSingleton = EnumSingleton.INSTANCE;

        //通過反射創建的非單例對象
        EnumSingleton newSingleton = EnumSingleton.class.getDeclaredConstructor().newInstance();
        //打印結果看看這兩個對象是否是一個
        System.out.println(originalSingleton == newSingleton);

    }
  • 打印結果
Exception in thread "main" java.lang.NoSuchMethodException: org.jc.framework.javasupport.EnumSingleton.<init>(int)
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at org.jc.framework.javasupport.EnumSingleton.main(EnumSingleton.java:16)

通過以上測試我們知道枚舉類型在java中是無法通過反射來創建的,否則會拋異常;

序列化破解

  • 代碼
public static void main(String[] args) throws IOException, ClassNotFoundException {
        //直接獲取原單例對象
        EnumSingleton originalSingleton = EnumSingleton.INSTANCE;
        //通過序列化創建的非單例對象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("newSingleton"));
        oos.writeObject(originalSingleton);
        oos.flush();
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("newSingleton"));
        EnumSingleton newSingleton = (EnumSingleton) ois.readObject();
        ois.close();

        //打印結果看看這兩個對象是否是一個
        System.out.println(originalSingleton == newSingleton);
    }
  • 打印結果
true

在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定製的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法;

其次枚舉單例的線程安全性是通過jvm的類加載機制來保證的,無需我們操心,但是對我來說,枚舉單例的懶加載是值得商榷的一點,如果枚舉中定義了靜態變量/常量/方法的話,那麼當使用的時候可能會破壞單例的懶加載!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章