设计模式 | 挑战单例模式(六)

437 阅读3分钟

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

内容接着上篇文章《挑战单例模式(五)》,上篇文章讲到了反射破坏单例,这篇文章我们来讲序列化也会破坏单例。

八、序列化破坏单例

上一章说到反射会破坏单例,那是不是只有这样的方式会破坏呢?答案是否定的是,序列化可以避开正常的对象实例化构造,一样会破坏单例模式。

关于Java对象序列化机制,就是一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。序列化的对象可以通过反序列化后在内存中新建一个对象。所有需要实现Java字节序列化的对象需要 implements java.io.Serializable

在通过Java序列化破坏单例的过程中,我们将对象创建好以后,将对象序列化写入磁盘,下一次使用的时候再从磁盘中读取到序列化内容,然后再反序列化转为内存对象。反序列化的对象会重新分配内存,即重新创建,所以通过这样的方式去序列化再反序列化单例对象,就可能会实例化多个对象,破坏了单例。

我们重新一个可序列化的饱汉式单例,如下面的代码所示。

public class SerializableDestroySingleton implements Serializable {
​
    private SerializableDestroySingleton(){
        if(INSTANCE != null){
            throw new AssertionError();
        }
        INSTANCE = this;
    }
​
    private static  SerializableDestroySingleton INSTANCE = new SerializableDestroySingleton();
​
    public static SerializableDestroySingleton getInstance(){
        return INSTANCE;
    }
}

跟之前实现的饱汉式单例最大的区别就是类implements java.io.Serializable

我们写一个测试main方法。

​
    public static void main(String[] args) {
        SerializableDestroySingleton instance1 = null;
        SerializableDestroySingleton instance2 = SerializableDestroySingleton.getInstance();
        try (FileOutputStream fos = new FileOutputStream("out.se");
             ObjectOutputStream oos = new ObjectOutputStream(fos);
             FileInputStream fis = new FileInputStream("out.se");
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            oos.writeObject(instance2);
            oos.flush();
            instance1 = (SerializableDestroySingleton)ois.readObject();
            System.out.println(instance1);
            System.out.println(instance2);
            System.out.println(instance1 == instance2);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

执行结果如下。

s1.png

从结果分析,反序列化后,是在内存上重新生成的实例对象,这样就破坏了实现单例的初衷。

那怎么解决这个问题呢?

可以在单例类中加一个私有方法,如下面的代码所示。

    private Object readResolve(){
        return INSTANCE;
    }

运行结果如下。

s2.png

这个是什么原理呢?如果要讲明白的话,要费不少时间,跟本文的主题有差距。简单说一下,对象还是实例化了两次,只是,通过反射技术,保证第二次获取的对象任然是第一次实例化的对象,如果对原理感兴趣的同学,可以写代码去调试一下,核心关注invokeReadResolve()方法就能明白。

所以其实上面的方法没有从根本上满足单例在程序运行期间,只实例化一个对象的需求,而且通过这样的方式,如果创建对象的很多,那么必然对内存的开销带来不小的挑战。那么什么能根本上解决这个问题?答案是注册式单例