theme: channing-cyan
这是我参与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(); } }
执行结果如下。
从结果分析,反序列化后,是在内存上重新生成的实例对象,这样就破坏了实现单例的初衷。
那怎么解决这个问题呢?
可以在单例类中加一个私有方法,如下面的代码所示。
private Object readResolve(){ return INSTANCE; }
运行结果如下。
这个是什么原理呢?如果要讲明白的话,要费不少时间,跟本文的主题有差距。简单说一下,对象还是实例化了两次,只是,通过反射技术,保证第二次获取的对象任然是第一次实例化的对象,如果对原理感兴趣的同学,可以写代码去调试一下,核心关注invokeReadResolve()
方法就能明白。
所以其实上面的方法没有从根本上满足单例在程序运行期间,只实例化一个对象的需求,而且通过这样的方式,如果创建对象的很多,那么必然对内存的开销带来不小的挑战。那么什么能根本上解决这个问题?答案是注册式单例。