一文教你如何破坏单例和防止单例被破坏

311 阅读4分钟

这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战

概述

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,如何进行破坏单例模式的话,通俗的来讲,创建两个或两个以上的实例就算是破坏了单例模式。既然知道了如何破坏单例模式,那么我们也应该知道如果防止单例模式被破坏?所以这篇文章我会带着大家来了解一下如何破坏单例模式和防止单例模式被破坏。

反射破坏单例模式

反射破坏单例代码

public class ReflectTest {
    public static void main(String[] args) {
        try {
            Class<?> clazz = LazyStaticClassSingleton.class;
            Constructor<?> clazzDeclaredConstructor = clazz.getDeclaredConstructor(null);
            clazzDeclaredConstructor.setAccessible(true);
            Object instance1 = clazzDeclaredConstructor.newInstance();
            Object instance2 = clazzDeclaredConstructor.newInstance();
            System.out.println(instance1);
            System.out.println(instance2);
            System.out.println(instance1 == instance2);
            System.out.println(clazzDeclaredConstructor);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

结果

image.png

通过反射获取到LazyStaticClassSingleton的对象,然后获取到私有的构造方法,通过setAccessible设置强制访问LazyStaticClassSingleton的对象的所有属性和方法,初始化instance1和instance2,并进行对比,通过结果发现,生成了两个不同的LazyStaticClassSingleton的对象,说明内存地址不同,就是实例化了多次,破坏了单例模式的特性。

如何防止反射破坏单例模式

通过上面反射破坏单例模式的代码,我们可以知道,反射也是通过调用构造方法来实例化对象,那么我们可以在构造函数里面做点事情来防止反射,我们把静态内部类单例的代码改造一下,看代码

private LazyStaticClassSingleton() {
    if (LazyHolder.INSTANCE != null){
        throw new RuntimeException("违反了单例模式");
    }
}

通过在私有的LazyStaticClassSingleton初始化方法中判断是存在实例,如果存在就抛出运行时异常。

序列化破坏单例模式

为什么单例模式使用序列化

一般情况下,在将一个单例模式创建后,需要将对象序列化写入到磁盘中,下一次使用直接从磁盘中读取对象并将序列化文件进行反序列化将该对象转换为内存对象。反序列化后会重新分配内存空间和地址,就是重新创建对象,如果序列化的目标对象是单例对象,那么破坏了单例,用序列化的方式,需要在静态内部类(LazyInnerClassSingleton) 实现 Serializable 接口,代码如下

public class SerializableSingleton implements Serializable {
    public  final static SerializableSingleton INSTANCE = new SerializableSingleton();
    private SerializableSingleton(){}

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

}
  • 结果

image.png

从运行结果中可以看出,反序列化后和手动创建的对象实例是不一样的,同一个对象实例化了两次,创建了两个对象,这明星违背了单例模式的原则。

  • 序列化的操作流程

    • 内存对象转换为字节码形式

    • 字节码通过IO输出流写入到磁盘中

  • 反序列化

    • 持久化的字节码内容,通过IO输入流读到内存中

    • 将内存对象转换为Java对象

如何防止序列化破坏单例模式

既然已经知道了如何使用序列化破坏单例模式,那么我们就需要在序列化文件中添加readResolve方法。

private Object readResolve(){ return INSTANCE;}
  • 结果

image.png

仅仅只添加readResolve方法就解决了序列化破坏问题,那么通过jdk的源码就可以找到答案,找到ObjectInputStream方法对中的readResolve方法,readResolve方法中找到readObject0方法,在readObject0方法中找到了checkResolve(readOrdinaryObject(unshared))方法,通过readOrdinaryObject方法找到了hasReadResolveMethod方法,而desc.hasReadResolveMethod()是这关键部分 ,这段代码的意思是查看你的单例类里面有没有readResolve方法,有的话就利用反射的方式执行这个方法,具体是desc.invokeReadResolve(obj)这段代码,返回单例对象。这里其实是实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,这也算是一个缺点吧。