写在前面
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
说人话单例类就是在一次 Java 程序生命周期中某个类的实例只有一个(单例类不对外提供创建实例的方法和构造方法)。
这里不讨论如何去声明一个效率最高的单例类,只讨论如何去破坏一个单例类。看下下面的代码:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
这是一个最简单的单例类,且是线程安全的。声明一个单例类的首要一点就是对外声明一个私有的构造器,这样在类外部就无法去调用该类的构造器去声明一个实例,只能去调用该类对外提供的 getInstance()
方法去获取该类的实例。不管获取多少次,返回的实例都是同一个,这样就达到的单例的效果。
不过接下来将会讨论如何去破坏这个单例类以及如何去保护这个单例类。
利用反射破坏单例
反射主要是利用操作字节码的技术,我们可以通过反射的方式获取 Singleton
的构造函数,然后设置这个构造函数的访问权限。之后利用构造函数实例化得出的新对象,将这个新对象与实例对象 INSTANCE
比较之后你就会发现它不是同一个对象,这样就达到破坏单例的效果,测试类如下:
public class SingletonTest {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {
Singleton instance = Singleton.getInstance();
Class<Singleton> clazz = (Class<Singleton>) Class.forName(Singleton.class.getName());
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
// 设置构造器访问权限
constructor.setAccessible(true);
// 实例化
Singleton instanceByReflect = constructor.newInstance();
System.out.println("Singleton: " + instance);
System.out.println("SingletonByReflect: " + instanceByReflect);
System.out.println("Singleton == SingletonByReflect: " + (instance == instanceByReflect));
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
输出结果:
Singleton: com.tencent.goff.singleton.Singleton@470e2030
SingletonByReflect: com.tencent.goff.singleton.Singleton@3fb4f649
Singleton == SingletonByReflect: false
从输出结果很容易看到声明的单例对象在内存中的地址是:
Singleton: com.tencent.goff.singleton.Singleton@470e2030
而利用反射去实例化得到的实例地址是:
SingletonByReflect: com.tencent.goff.singleton.Singleton@3fb4f649
从这一点信息中就可以看出两个实例并不是同一个实例对象。
那如何解决呢?
我们知道创建新对象必须调用构造方法,那么我们就在构造方法中进行一次判断,当实例对象 INSTANCE
不为 NULL 时就抛出一个异常不让它去实例化就好了。所以只需要修改下构造方法:
private Singleton() {
if (Objects.nonNull(INSTANCE)) {
throw new RuntimeException("单例类不允许被重复实例化");
}
}
这样,当我们再利用反射去实例化对象时就会抛出如下异常,也就能保证单例实例唯一了。
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.tencent.goff.singleton.SingletonTest.main(SingletonTest.java:18)
Caused by: java.lang.RuntimeException: 单例类不允许被重复实例化
at com.tencent.goff.singleton.Singleton.<init>(Singleton.java:11)
... 5 more
利用序列化破坏单例
除了利用反射之外,我们还可以通过序列化的方式去破坏单例,首先修改单例类 Singleton
让其实现序列化接口标识:
public class Singleton implements Serializable {
private static final long serialVersionUID = 3868696324377126263L;
// 其他略 ...
}
然后序列化该单例类之后再进行一次反序列化:
public class SingletonTest {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("tempFile"))) {
outputStream.writeObject(instance);
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("tempFile"))) {
Singleton readObject = (Singleton) inputStream.readObject();
System.out.println("Singleton: " + instance);
System.out.println("readObject: " + readObject);
System.out.println("Singleton == readObject: " + (instance == readObject));
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果如下:
Singleton: com.tencent.goff.singleton.Singleton@244038d0
readObject: com.tencent.goff.singleton.Singleton@44e81672
Singleton == readObject: false
所以,利用序列化也能过去破坏单例。怎么解决呢?简单,直接在单例类中增加一个 readResolve
方法即可,如下:
private Object readResolve() {
return INSTANCE;
}
之后再次执行测试就会发现单例类实例唯一了:
Singleton: com.tencent.goff.singleton.Singleton@244038d0
readObject: com.tencent.goff.singleton.Singleton@244038d0
Singleton == readObject: true
那为什么是 readResolve
方法呢?这就要查看 java.io.ObjectInputStream#readObject()
方法的源码了,源码不多说,这里只提示最后会进入 java.io.ObjectInputStream#readOrdinaryObject
方法,在该方法中会调用 java.io.ObjectStreamClass#invokeReadResolve
方法,下面是具体的调用栈,无关代码略:
public class ObjectInputStream {
public final Object readObject() throws IOException, ClassNotFoundException {
return readObject(Object.class);
}
private final Object readObject(Class<?> type) throws IOException, ClassNotFoundException {
// ...
Object obj = readObject0(type, false);
}
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
try {
switch (tc) {
// ...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
// ...
}
} finally {
// ...
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
ObjectStreamClass desc = readClassDesc(false);
// ...
Object rep = desc.invokeReadResolve(obj);
return obj;
}
}
最后调用了 java.io.ObjectStreamClass#invokeReadResolve
方法:
Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException {
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
// ...
}
} else {
throw new UnsupportedOperationException();
}
}
可以看到,最后会判断是否有 readResolve
方法,如果有该方法就直接调用。否则的话怎么办?反射呀!!!!