被破坏的单例模式

152 阅读5分钟

大家好,我是 codeyang~ 公众号【codeyang】记得点赞关注 😁

今天我们继续学习下单例模式,上一篇介绍了单例模式的常见创建方式以及线程安全的问题。想必大家在开发中碰见需要创建单例对象时肯定会考虑同步方法、双重检查锁定以及枚举等方式。

如果你恰巧没有使用枚举方式,那么下面介绍的两种方式就会破坏单例,创建多个对象!

打破唯一实例

序列化和反序列化

问题复现:我们将单例对象序列化到磁盘,之后从磁盘读取反序列化成对象,多次读取看是否是同一个对象?

静态内部类的方式获取单例

package com.yang.pattern.singleton.case08;

import java.io.Serializable;

/**
 * @Description: 静态内部类实现单例
 * @Author :  公众号codeyang
 * @Date : Created 2023/5/29 10:15 上午
 */
public class Singleton implements Serializable {

    private Singleton() {
    }

    private static class singletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance () {
        return singletonHolder.INSTANCE;
    }


}

序列化反序列化破坏单例

package com.yang.pattern.singleton.case08;

import java.io.*;

/**
 * @Description: 序列化与反序列化
 * @Author :  公众号codeyang
 * @Date : Created 2023/5/29 10:22 上午
 */
public class BreakModeOne {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        //writeObjectToFile();
        Singleton singleton1 = readObjectFromFile();
        Singleton singleton2 = readObjectFromFile();

        System.out.println(singleton1 == singleton2);

    }

    /**
     * 将单例对象写入磁盘 /Users/yang/Desktop/test.txt (Windows下更换自己的路径)
     */
    public static void writeObjectToFile() throws IOException {
        //获取单例对象
        Singleton instance = Singleton.getInstance();

        //创建输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/Users/yang/Desktop/test.txt"));
        //将对象写入到文件中
        oos.writeObject(instance);
        oos.close();
    }

    /**
     * 从文件反序列化得到对象
     */

    public static Singleton readObjectFromFile() throws IOException, ClassNotFoundException {

        //创建输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/Users/yang/Desktop/test.txt"));

        //读取对象
        Singleton instance = (Singleton) ois.readObject();

        return instance;

    }

}

结果

test.txt 文件为二进制文件,不需要打开。由于编码的问题也无法查看,不用关心里面的具体内容。

通过读取序列化后的 text.txt 反序列化后获取实例对象,比较两次结果为 false,说明序列化反序列化破坏了单例

反射

通过反射方式获取 Singleton 类的实例,验证其是否为单例

反射方式破坏单例

package com.yang.pattern.singleton.case08;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @Description: 反射方式破坏单例
 * @Author :  公众号codeyang
 * @Date : Created 2023/5/29 11:01 上午
 */
public class BreakModeTwo {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //获取Singleton类的字节码对象
        Class clazz = Singleton.class;

        //获取私有无餐构造对象
        Constructor constructor = clazz.getDeclaredConstructor();

        //取消访问检查
        constructor.setAccessible(true);

        //创建singleton对象
        Singleton instance1 = (Singleton) constructor.newInstance();
        Singleton instance2 = (Singleton) constructor.newInstance();

        System.out.println(instance1 == instance2);

    }
}

结果

反射方式获取的实例对象比较结果 false,同样证明反射也会破坏单例

枚举类方式不会出现序列化反序列化、反射单例被破坏的问题

解决单例被破坏问题

反序列化破坏单例问题解决

序列化和反序列化破坏单例问题可以通过在 Singleton 类中添加 readResolve (),确保在反序列化时返回同一个实例,从而维护单例的唯一性

package com.yang.pattern.singleton.case08;

import java.io.Serializable;

/**
 * @Description: 静态内部类实现单例
 * @Author :  公众号codeyang
 * @Date : Created 2023/5/29 10:15 上午
 */
public class Singleton implements Serializable {

    private Singleton() {
    }

    private static class singletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance () {
        return singletonHolder.INSTANCE;
    }

    //解决序列化反序列化破坏单例问题
    private Object readResolve () {
        return singletonHolder.INSTANCE;
    }
}

结果

加了 readResolve ( )就能解决问题的原因,是由于当对象被反序列化时,Java 虚拟机会检查类中是否存在 readResolve ( )方法,并在反序列化过程中调用它。通过在 readResolve ( )方法中返回实例,避免每次新生成一个实例,保证了单例的唯一性。

简单看下源码

//源码 分析

//debug进readObject
    private final Object readObject(Class<?> type)
        throws IOException, ClassNotFoundException
    {
        ...
        try {
          //debug 进readObject0查看
            Object obj = readObject0(type, false);
        ...
    }


 private Object readObject0(Class<?> type, boolean unshared) throws IOException {
   ...
    try {
  switch (tc) {
   ...
   case TC_OBJECT:
    return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
   ...
        }
    } finally {
        depth--;
        bin.setBlockDataMode(oldMode);
    }

 }


private Object readOrdinaryObject(boolean unshared)
        throws IOException
{
    ...
     Object obj;
    try {
       // desc.isInstantiable()判断返回true,通过反射调用 newInstance()创建新的实例
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        }

  ...
  if (obj != null &&
            handles.lookupException(passHandle) == null &&
         //判断Singleton类中是否添加了readRsolve () 方法
            desc.hasReadResolveMethod())
        {
        // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
          // 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
            Object rep = desc.invokeReadResolve(obj);
}

通过源码证明了,在 Singleton 类中添加 readResolve ( )方法返回实例可以解决反序列化破坏单例问题。

反射破坏单例的问题解

反射在获取了构造器对象,并绕过了私有方法限制可以创建多个实例。但是我们可以在构造器中添加逻辑判断,当检查到已经存在实例时,就抛出异常或返回现有实例。这样即使是通过反射创建实例,也能保证实例的唯一性。

package com.yang.pattern.singleton.case08;

import java.io.Serializable;

/**
 * @Description: 静态内部类实现单例
 * @Author :  公众号codeyang
 * @Date : Created 2023/5/29 10:15 上午
 */
public class Singleton implements Serializable {

    private Singleton() {
        //添加判断,存在实例抛出异常保证实例唯一
        if (singletonHolder.INSTANCE != null){
            throw new RuntimeException("实例已经被创建!");
        }
    }

    private static class singletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance () {
        return singletonHolder.INSTANCE;
    }

}

结果

由此可见通过枚举方式实现单例模式,真是 yyds

JDK 中的 Runtime 类就是使用的单例设计模式,而且还是饿汉式实现的

Runtime-饿汉式

感谢阅读 😁,这次就分享到这里,记得点赞、收藏加关注~

往期推荐

你必须掌握的设计模式:单例模式

设计模式七大原则