使用枚举来实现单例模式

单例模式的实现方式有很多种,详情可以参考单例模式的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的类加载机制来保证的,无需我们操心,但是对我来说,枚举单例的懒加载是值得商榷的一点,如果枚举中定义了静态变量/常量/方法的话,那么当使用的时候可能会破坏单例的懒加载!

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