反射破坏单例

大家有没有发现,上面介绍的单例模式的构造方法除了加上 private 关键字,没有做任何处理。如果我们使用反射来调用其构造方法,在调用 getInstance() 方法,应该有两个不同的实例。现在来看一段测试代码,以 LazyInnerClassSingleton 为例:

  1. public class LazyInnerClassSingletonTest {
  2. public static void main(String[] args) {
  3. try {
  4. // 进行破坏
  5. Class<?> clazz = LazyInnerClassSingleton.class;
  6. // 通过反射获取私有的构造方法
  7. Constructor c = clazz.getDeclaredConstructor(null);
  8. // 强制访问
  9. c.setAccessible(true);
  10. // 暴力初始化
  11. Object o1 = c.newInstance();
  12. // 调用了两次构造方法,相当于“new”了两次
  13. Object o2 = c.newInstance();
  14. System.out.println(o1 == o2);
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }

运行结果如下图所示。

image.png

显然,创建了两个不同的实例。现在,我们在其构造方法中做了一些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码:

  1. public class LazyInnerClassSingleton {
  2. private LazyInnerClassSingleton() {
  3. if (LazyHolder.INSTANCE != null) {
  4. throw new RuntimeException("不允许创建多个实例");
  5. }
  6. }
  7. // 注意关键字final,保证方法不被重写和重载
  8. public static final LazyInnerClassSingleton getInstance() {
  9. // 在返回结果之前,会先加载内部类
  10. return LazyHolder.INSTANCE;
  11. }
  12. // 默认不加载
  13. private static class LazyHolder {
  14. private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
  15. }
  16. }

再运行测试代码,会得到如下图所示结果。

image.png

序列化破坏单例

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:

  1. public class SeriableSingleton implements Serializable {
  2. // 序列化就是把内存中的状态通过转换成字节码的形式
  3. // 从而转换一个 I/O 流,写入其他地方(可以是磁盘、网络 I/O)
  4. // 内存中的状态会永久保存下来
  5. // 反序列化就是将已经持久化的字节码内容转换为I/O流
  6. // 通过I/O流的读取,进而将读取的内容转换为Java对象
  7. // 在转换过程中会重新创建对象new
  8. private SeriableSingleton(){}
  9. private static final SeriableSingleton instance = new SeriableSingleton();
  10. public static SeriableSingleton getInstance(){
  11. return instance;
  12. }
  13. }

编写测试代码:

  1. public class SeriableSingletonTest {
  2. public static void main(String[] args) {
  3. SeriableSingleton s1 = null;
  4. SeriableSingleton s2 = SeriableSingleton.getInstance();
  5. FileOutputStream fos = null;
  6. try {
  7. fos = new FileOutputStream("SeriableSingleton.obj");
  8. ObjectOutputStream oos = new ObjectOutputStream(fos);
  9. oos.writeObject(s2);
  10. oos.flush();
  11. oos.close();
  12. FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
  13. ObjectInputStream ois= new ObjectInputStream(fis);
  14. s1 = (SeriableSingleton) ois.readObject();
  15. ois.close();
  16. fis.close();
  17. System.out.println(s1 == s2);
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. }

运行结果如下图所示:

image.png

从运行结果来看,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例模式呢?其实很简单,只需要增加 readResolve() 方法即可。来看优化后的代码:

  1. public class SeriableSingleton implements Serializable {
  2. private SeriableSingleton(){}
  3. private static final SeriableSingleton instance = new SeriableSingleton();
  4. public static SeriableSingleton getInstance(){
  5. return instance;
  6. }
  7. private Object readResolve(){
  8. return instance;
  9. }
  10. }

再看运行结果,如下图所示。

image.png

为什么增加了一个 readResolve() 方法后,就能避免序列化破坏单例呢?我们一起来看看 JDK 的源码实现。进入 ObjectInputStream 类的 readObject() 方法,代码如下:

  1. public final Object readObject()
  2. throws IOException, ClassNotFoundException
  3. {
  4. if (enableOverride) {
  5. return readObjectOverride();
  6. }
  7. // if nested read, passHandle contains handle of enclosing object
  8. int outerHandle = passHandle;
  9. try {
  10. Object obj = readObject0(false);
  11. handles.markDependency(outerHandle, passHandle);
  12. ClassNotFoundException ex = handles.lookupException(passHandle);
  13. if (ex != null) {
  14. throw ex;
  15. }
  16. if (depth == 0) {
  17. vlist.doCallbacks();
  18. }
  19. return obj;
  20. } finally {
  21. passHandle = outerHandle;
  22. if (closed && depth == 0) {
  23. clear();
  24. }
  25. }
  26. }

我们发现,在 readObject() 方法中又调用了重写的 readObject0() 方法。进入 readObject0() 方法,代码如下:

  1. private Object readObject0(boolean unshared) throws IOException {
  2. boolean oldMode = bin.getBlockDataMode();
  3. ...
  4. case TC_OBJECT:
  5. return checkResolve(readOrdinaryObject(unshared));
  6. ...
  7. }

我们看到 TC_OBJECT 中调用了 ObjectInputStream 的 readOrdinaryObject() 方法,看源码:

  1. private Object readOrdinaryObject(boolean unshared)
  2. throws IOException
  3. {
  4. if (bin.readByte() != TC_OBJECT) {
  5. throw new InternalError();
  6. }
  7. ObjectStreamClass desc = readClassDesc(false);
  8. desc.checkDeserialize();
  9. Class<?> cl = desc.forClass();
  10. if (cl == String.class || cl == Class.class
  11. || cl == ObjectStreamClass.class) {
  12. throw new InvalidClassException("invalid class descriptor");
  13. }
  14. Object obj;
  15. try {
  16. obj = desc.isInstantiable() ? desc.newInstance() : null;
  17. } catch (Exception ex) {
  18. throw (IOException) new InvalidClassException(
  19. desc.forClass().getName(),
  20. "unable to create instance").initCause(ex);
  21. }
  22. ...
  23. return obj;
  24. }

我们发现调用了 ObjectStreamClass 的 isInstantiable() 方法,而 isInstantiable() 方法的代码如下:

  1. boolean isInstantiable() {
  2. requireInitialized();
  3. return (cons != null);
  4. }

上述代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true。这意味着只要有无参构造方法就会实例化。

这时候其实还没有找到加上 readResolve() 方法就避免了单例模式被破坏的真正原因。再回到 ObjectInputStream 的 readOrdinaryObject() 方法,继续往下看:

  1. private Object readOrdinaryObject(boolean unshared)
  2. throws IOException
  3. {
  4. if (bin.readByte() != TC_OBJECT) {
  5. throw new InternalError();
  6. }
  7. ObjectStreamClass desc = readClassDesc(false);
  8. desc.checkDeserialize();
  9. Class<?> cl = desc.forClass();
  10. if (cl == String.class || cl == Class.class
  11. || cl == ObjectStreamClass.class) {
  12. throw new InvalidClassException("invalid class descriptor");
  13. }
  14. Object obj;
  15. try {
  16. obj = desc.isInstantiable() ? desc.newInstance() : null;
  17. } catch (Exception ex) {
  18. throw (IOException) new InvalidClassException(
  19. desc.forClass().getName(),
  20. "unable to create instance").initCause(ex);
  21. }
  22. ...
  23. if (obj != null &&
  24. handles.lookupException(passHandle) == null &&
  25. desc.hasReadResolveMethod())
  26. {
  27. Object rep = desc.invokeReadResolve(obj);
  28. if (unshared && rep.getClass().isArray()) {
  29. rep = cloneArray(rep);
  30. }
  31. if (rep != obj) {
  32. // Filter the replacement object
  33. if (rep != null) {
  34. if (rep.getClass().isArray()) {
  35. filterCheck(rep.getClass(), Array.getLength(rep));
  36. } else {
  37. filterCheck(rep.getClass(), -1);
  38. }
  39. }
  40. handles.setObject(passHandle, obj = rep);
  41. }
  42. }
  43. return obj;
  44. }

判断无参构造方法是否存在之后,有调用了 hasReadResolveMethod() 方法,来看代码:

  1. boolean hasReadResolveMethod() {
  2. requireInitialized();
  3. return (readResolveMethod != null);
  4. }

上述代码逻辑非常简单,就是判断 readResolveMethod 是否为空,不为空就返回 true。那么 readResolveMethod 是在哪里赋值的呢?通过全局查找知道,在私有方法 ObjectStreamClass() 中给 readResolveMethod 进行了赋值,来看代码:

  1. readResolveMethod = getInheritableMethod(
  2. cl, "readResolve", null, Object.class);

上面的逻辑其实就是通过反射找到一个无参的 readResolve() 方法,并且保存下来。现在回到 ObjectInputStream 的 readOrdinaryObject() 方法继续往下看,如果 readResolve() 方法存在则调用 invokeReadResolve() 方法,来看代码:

  1. Object invokeReadResolve(Object obj)
  2. throws IOException, UnsupportedOperationException
  3. {
  4. requireInitialized();
  5. if (readResolveMethod != null) {
  6. try {
  7. return readResolveMethod.invoke(obj, (Object[]) null);
  8. } catch (InvocationTargetException ex) {
  9. Throwable th = ex.getTargetException();
  10. if (th instanceof ObjectStreamException) {
  11. throw (ObjectStreamException) th;
  12. } else {
  13. throwMiscException(th);
  14. throw new InternalError(th); // never reached
  15. }
  16. } catch (IllegalAccessException ex) {
  17. // should not occur, as access checks have been suppressed
  18. throw new InternalError(ex);
  19. }
  20. } else {
  21. throw new UnsupportedOperationException();
  22. }
  23. }

我们可以看出,在 invokeReadResolve() 方法中用反射调用了 readResolveMethod() 方法。

在 readOrdinaryObject() 方法的最后把 readResolve() 方法返回的对象赋给了 obj 变量,并返回。

通过 JDK 源码分析我们可以看出,虽然增加 readResolve() 返回实例解决了单例模式被破坏的问题。但实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率加快,就意味着内存分配开销也会随之增大,难道真的就没有办法从根本上解决问题么?单例模式中的注册式单例能解决该问题。

摘录:《Spring 5 核心原理与30个类手写实战》来自文艺界的Tom老师的书籍。

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/uxggg3 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。