反射破坏单例
大家有没有发现,上面介绍的单例模式的构造方法除了加上 private 关键字,没有做任何处理。如果我们使用反射来调用其构造方法,在调用 getInstance() 方法,应该有两个不同的实例。现在来看一段测试代码,以 LazyInnerClassSingleton 为例:
public class LazyInnerClassSingletonTest {public static void main(String[] args) {try {// 进行破坏Class<?> clazz = LazyInnerClassSingleton.class;// 通过反射获取私有的构造方法Constructor c = clazz.getDeclaredConstructor(null);// 强制访问c.setAccessible(true);// 暴力初始化Object o1 = c.newInstance();// 调用了两次构造方法,相当于“new”了两次Object o2 = c.newInstance();System.out.println(o1 == o2);} catch (Exception e) {e.printStackTrace();}}}
运行结果如下图所示。

显然,创建了两个不同的实例。现在,我们在其构造方法中做了一些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码:
public class LazyInnerClassSingleton {private LazyInnerClassSingleton() {if (LazyHolder.INSTANCE != null) {throw new RuntimeException("不允许创建多个实例");}}// 注意关键字final,保证方法不被重写和重载public static final LazyInnerClassSingleton getInstance() {// 在返回结果之前,会先加载内部类return LazyHolder.INSTANCE;}// 默认不加载private static class LazyHolder {private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();}}
再运行测试代码,会得到如下图所示结果。

序列化破坏单例
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:
public class SeriableSingleton implements Serializable {// 序列化就是把内存中的状态通过转换成字节码的形式// 从而转换一个 I/O 流,写入其他地方(可以是磁盘、网络 I/O)// 内存中的状态会永久保存下来// 反序列化就是将已经持久化的字节码内容转换为I/O流// 通过I/O流的读取,进而将读取的内容转换为Java对象// 在转换过程中会重新创建对象newprivate SeriableSingleton(){}private static final SeriableSingleton instance = new SeriableSingleton();public static SeriableSingleton getInstance(){return instance;}}
编写测试代码:
public class SeriableSingletonTest {public static void main(String[] args) {SeriableSingleton s1 = null;SeriableSingleton s2 = SeriableSingleton.getInstance();FileOutputStream fos = null;try {fos = new FileOutputStream("SeriableSingleton.obj");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(s2);oos.flush();oos.close();FileInputStream fis = new FileInputStream("SeriableSingleton.obj");ObjectInputStream ois= new ObjectInputStream(fis);s1 = (SeriableSingleton) ois.readObject();ois.close();fis.close();System.out.println(s1 == s2);} catch (Exception e) {e.printStackTrace();}}}
运行结果如下图所示:

从运行结果来看,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例模式呢?其实很简单,只需要增加 readResolve() 方法即可。来看优化后的代码:
public class SeriableSingleton implements Serializable {private SeriableSingleton(){}private static final SeriableSingleton instance = new SeriableSingleton();public static SeriableSingleton getInstance(){return instance;}private Object readResolve(){return instance;}}
再看运行结果,如下图所示。

为什么增加了一个 readResolve() 方法后,就能避免序列化破坏单例呢?我们一起来看看 JDK 的源码实现。进入 ObjectInputStream 类的 readObject() 方法,代码如下:
public final Object readObject()throws IOException, ClassNotFoundException{if (enableOverride) {return readObjectOverride();}// if nested read, passHandle contains handle of enclosing objectint outerHandle = passHandle;try {Object obj = readObject0(false);handles.markDependency(outerHandle, passHandle);ClassNotFoundException ex = handles.lookupException(passHandle);if (ex != null) {throw ex;}if (depth == 0) {vlist.doCallbacks();}return obj;} finally {passHandle = outerHandle;if (closed && depth == 0) {clear();}}}
我们发现,在 readObject() 方法中又调用了重写的 readObject0() 方法。进入 readObject0() 方法,代码如下:
private Object readObject0(boolean unshared) throws IOException {boolean oldMode = bin.getBlockDataMode();...case TC_OBJECT:return checkResolve(readOrdinaryObject(unshared));...}
我们看到 TC_OBJECT 中调用了 ObjectInputStream 的 readOrdinaryObject() 方法,看源码:
private Object readOrdinaryObject(boolean unshared)throws IOException{if (bin.readByte() != TC_OBJECT) {throw new InternalError();}ObjectStreamClass desc = readClassDesc(false);desc.checkDeserialize();Class<?> cl = desc.forClass();if (cl == String.class || cl == Class.class|| cl == ObjectStreamClass.class) {throw new InvalidClassException("invalid class descriptor");}Object obj;try {obj = desc.isInstantiable() ? desc.newInstance() : null;} catch (Exception ex) {throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);}...return obj;}
我们发现调用了 ObjectStreamClass 的 isInstantiable() 方法,而 isInstantiable() 方法的代码如下:
boolean isInstantiable() {requireInitialized();return (cons != null);}
上述代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true。这意味着只要有无参构造方法就会实例化。
这时候其实还没有找到加上 readResolve() 方法就避免了单例模式被破坏的真正原因。再回到 ObjectInputStream 的 readOrdinaryObject() 方法,继续往下看:
private Object readOrdinaryObject(boolean unshared)throws IOException{if (bin.readByte() != TC_OBJECT) {throw new InternalError();}ObjectStreamClass desc = readClassDesc(false);desc.checkDeserialize();Class<?> cl = desc.forClass();if (cl == String.class || cl == Class.class|| cl == ObjectStreamClass.class) {throw new InvalidClassException("invalid class descriptor");}Object obj;try {obj = desc.isInstantiable() ? desc.newInstance() : null;} catch (Exception ex) {throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);}...if (obj != null &&handles.lookupException(passHandle) == null &&desc.hasReadResolveMethod()){Object rep = desc.invokeReadResolve(obj);if (unshared && rep.getClass().isArray()) {rep = cloneArray(rep);}if (rep != obj) {// Filter the replacement objectif (rep != null) {if (rep.getClass().isArray()) {filterCheck(rep.getClass(), Array.getLength(rep));} else {filterCheck(rep.getClass(), -1);}}handles.setObject(passHandle, obj = rep);}}return obj;}
判断无参构造方法是否存在之后,有调用了 hasReadResolveMethod() 方法,来看代码:
boolean hasReadResolveMethod() {requireInitialized();return (readResolveMethod != null);}
上述代码逻辑非常简单,就是判断 readResolveMethod 是否为空,不为空就返回 true。那么 readResolveMethod 是在哪里赋值的呢?通过全局查找知道,在私有方法 ObjectStreamClass() 中给 readResolveMethod 进行了赋值,来看代码:
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
上面的逻辑其实就是通过反射找到一个无参的 readResolve() 方法,并且保存下来。现在回到 ObjectInputStream 的 readOrdinaryObject() 方法继续往下看,如果 readResolve() 方法存在则调用 invokeReadResolve() 方法,来看代码:
Object invokeReadResolve(Object obj)throws IOException, UnsupportedOperationException{requireInitialized();if (readResolveMethod != null) {try {return readResolveMethod.invoke(obj, (Object[]) null);} catch (InvocationTargetException ex) {Throwable th = ex.getTargetException();if (th instanceof ObjectStreamException) {throw (ObjectStreamException) th;} else {throwMiscException(th);throw new InternalError(th); // never reached}} catch (IllegalAccessException ex) {// should not occur, as access checks have been suppressedthrow new InternalError(ex);}} else {throw new UnsupportedOperationException();}}
我们可以看出,在 invokeReadResolve() 方法中用反射调用了 readResolveMethod() 方法。
在 readOrdinaryObject() 方法的最后把 readResolve() 方法返回的对象赋给了 obj 变量,并返回。
通过 JDK 源码分析我们可以看出,虽然增加 readResolve() 返回实例解决了单例模式被破坏的问题。但实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率加快,就意味着内存分配开销也会随之增大,难道真的就没有办法从根本上解决问题么?单例模式中的注册式单例能解决该问题。
摘录:《Spring 5 核心原理与30个类手写实战》来自文艺界的Tom老师的书籍。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/uxggg3 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
