反射破坏单例
大家有没有发现,上面介绍的单例模式的构造方法除了加上 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对象
// 在转换过程中会重新创建对象new
private 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 object
int 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 object
if (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 suppressed
throw 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 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。