1、序列化及反序列化对单例的破坏
2、反射机制对单例的破坏
3、使用枚举类型实现单例模式的原因
4、使用readResolve方法解决反序列化及其弊端
关于单例模式,我们可以使用静态内部类、双重检测的实现方法来保证线程安全,那么该如何保证单例模式最核心的作用——“实现该模式的类有且只有一个实例对象”呢?
我们知道,创建一个对象的方式有:new、克隆、序列化、反射。
通过上述分析,若要实现一个完美的单例模式必须考虑序列化和反射问题。本文就序列化和反射是如何破坏单例模式,以及枚举类型是如何完美解决这个问题加以解析讨论。
先写一个双重检测实现的单例模式。
再写一个测试:
系统输出:
为什么反序列化之后会生成一个新的对象,要分析这个问题,就要从源码中找答案。这里我们简要分析一下ObjectInputStream.java的源码。
首先给出一个方法调用栈:readObject—>readObject0—>readOrdinaryObject
分析源码中最重要的语句:
obj = desc.isInstantiable() ? desc.newInstance() : null;
其中desc是一个ObjectStreamClass类对象,再来看看isInstantiable()和newInstance()的源码实现。
boolean isInstantiable() {
return (cons != null);
}
Object newInstance()
throws InstantiationException, InvocationTargetException,
UnsupportedOperationException
{
if (cons != null) {
try {
return cons.newInstance();
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
ObjectStreamClass类中维护了一个Constructor私有变量cons:
private Constructor> cons;
如果某个类(本例中是Singleton04)是可序列化的,也就是实现了Serializable接口,那么cons将会在ObjectStreamClass的构造好书中被初始化成该类的一个无参构造器:
cons = getSerializableConstructor(cl);//c1就是Singleton04
由此往上推,desc.isInstantiable()将返回true,desc.newInstance()将返回Singleton04类的一个实例对象。至此说明了单例模式在反序列化的过程中将产生一个新的对象,单例模式被破坏。
系统输出结果;
由此可见,即便单例模式将构造函数声明为私有的,通过反射机制依然可以调用该私有构造器来创建对象。即便是在构造其中设置“防止多次实例化的代码”(比如设置一个flag)也于事无补,因为任然可以通过反射机制来修改flag,从而达到多次实例化的目的。
首先来写一个枚举类的单例模式实现:
写一个测试来分析反序列化结果:
结果:
再写一个测试来分析反射创建对象的结果:
结果为:
说明反射机制创建不了该类对象。在分析原因之前我们先看看枚举类实际上是怎么回事,通过反编译软件(我用的是DJ JAVA COMPLIER)对上述实现的单例模式的字节码文件进行反编译,其结果如下:
其中的values()方法保存了所有创建的枚举对象,在反序列化时发挥了重要的作用,下文会提到。
再来看看父类Enum的构造函数:
总结起来,所谓枚举其实是继承了Enum类的一个子类,枚举中可以声明多个对象,每个枚举对象拥有两个唯一的属性:String name 和 int ordinal,name就是我们在声明枚举变量是的名字(比如INSTANCE),ordinal就是声明的顺序(比如INSTANCE是第一个声明的,所以为0)。
明白这些就可以继续分析。
在java规范中对枚举类型的序列化和饭序列化做了特殊规定:
Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream;the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method,passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.The process by which enum constants are serialized cannot be customized: any class-specific writeObject,readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.
首先,在序列化和反序列化期间,任何特定于类的writeObject,readObject,readObjectNoData,writeReplace和readResolve方法都会被忽略。 同样,任何serialPersistentFields或serialVersionUID字段声明也会被忽略,所有枚举类型的fixedserialVersionUID都是0L。(也就是说枚举类型序列化反序列化机制与其他类型的不一样)。
其次,枚举对象的序列化、反序列化有自己的一套机制。序列化时,仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。
下面分析一下valueOf源码。
再来看看enumConstantDirectory()源码:
继续看getEnumConstantsShared()源码:
getEnumConstantsShared()方法获取枚举类的values()方法,然后得到枚举类所创建的所有枚举对象。
之前提到过,每个枚举对象都有一个唯一的name属性。序列化只是将name属性序列化,在反序列化的时候,通过创建一个Map(key,value),搭建起name和与之对应的对象之间的联系,然后通过索引key来获得枚举对象。
下面梳理一下枚举对象序列化,以及反序列化拿到对象的流程:
序列化:
反序列化:
通过反射机制不会创建新的枚举对象的原因就比较简单了,直接从Constructor类源码中便可看出:
以上说明了底层源码是不允许通过反射机制创建一个枚举对象的,因此保证了枚举类型实现单例模式的反射安全。
其实如果不使用枚举类实现单例,还有一种方法可以“保证”反序列化安全,那就是在类中类定一个readResolve方法,那么在反序列化之后,新建对象上的readResolve方法就会被调用。然后,该方法返回的对象引用将被返回,取代新建的对象(新建的对象也就是反序列化新生成的对象)。具体怎么回事呢?看源码:
下面我们具体看看这个readResolve()方法怎么使用。在原来的双检测单例模式中添加一个readResolve()方法:
再测试一下反序列化结果:
由此可见,若类中定义一个readResolve方法,其返回值将会替代之前创建的新对象,一次保证了反序列化后仍然是原来的对象。
但是与枚举实现相比,还是枚举实现具有更高的优先级。具体原因可以参考这篇文章:Effective Java之对于实例控制,枚举类型优于readResolve(七十七)
以上就是本文的全部分析,若有不足或不对之处望大家指出!