记录:反射与反序列化影响下的单例模式
单例模式在反射和反序列化的情况下会被破坏,创造出不止一个实例。
反射下的单例模式:
public class TestSingleClass1 {
private TestSingleClass1() {
}
public static class SingleInnerClass{
private static final TestSingleClass1 testSingleClass1 = new TestSingleClass1();
}
public static TestSingleClass1 getInstance() {
return SingleInnerClass.testSingleClass1;
}
public void test() {
System.out.println("testtest......");
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Class clazz = Class.forName("com.wmx.single.TestSingleClass1");
TestSingleClass1 tsc1 = (TestSingleClass1) clazz.newInstance();
TestSingleClass1 tsc2 = (TestSingleClass1) clazz.newInstance();
TestSingleClass1 tsc3 =TestSingleClass1.getInstance();
TestSingleClass1 tsc4 =TestSingleClass1.getInstance();
System.out.println(tsc1.hashCode());
System.out.println(tsc2.hashCode());
System.out.println(tsc3.hashCode());
System.out.println(tsc4.hashCode());
tsc1.test();
tsc2.test();
tsc3.test();
}
}
在这段代码下,用反射的方式创建了两个实例,然后用单例模式获得两次实例,然后打印它们的hashcode,运行结果如下:
后面两个hashcode打印出来是一样的,单例模式没有问题,前面的却都不一样,说明用反射生成的实例是全新的实例,这样就破坏了单例模式。
解决方法:增加标志记录,只有第一次构造方法被调用时才能执行,其他都会抛出异常:
public class TestSingleClass1 {
private static boolean flag = false;
//第一次调用后修改flag,注意要加同步锁,否则依然可能出来两个实例
private TestSingleClass1() {
synchronized (TestSingleClass1.class) {
if(flag == false) {
flag = true;
}else {
throw new RuntimeException("not singleton!");
}
}
}
public static class SingleInnerClass{
private static final TestSingleClass1 testSingleClass1 = new TestSingleClass1();
}
public static TestSingleClass1 getInstance() {
return SingleInnerClass.testSingleClass1;
}
public void test() {
System.out.println("testtest......");
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Class clazz = Class.forName("com.wmx.single.TestSingleClass1");
TestSingleClass1 tsc6666 = TestSingleClass1.getInstance();
TestSingleClass1 tsc1 = (TestSingleClass1) clazz.newInstance();
}
}
运行结果:
第二次调用构造方法的时候就报异常了,维持住了单例。
这里有一个问题就是需要保证获取单例先于反射执行,否则先反射的话,获取单例的getInstance方法就废掉了,效果就是调用方法就报异常。
反序列化影响单例:
public class TestSingleClass2 implements Serializable{
private static final long serialVersionUID = 7198203598944411394L;
private TestSingleClass2() {
}
public static class SingleInnerClass{
private static final TestSingleClass2 testSingleClass2 = new TestSingleClass2();
}
public static TestSingleClass2 getInstance() {
return SingleInnerClass.testSingleClass2;
}
public void test() {
System.out.println("testtest......");
}
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
//序列化对象
TestSingleClass2 tsc = TestSingleClass2.getInstance();
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("test.txt")));
oo.writeObject(tsc);
oo.close();
//反序列化对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("test.txt")));
TestSingleClass2 tsc1 = (TestSingleClass2) ois.readObject();
System.out.println(tsc.hashCode());
System.out.println(tsc1.hashCode());
}
}
一个实现了序列化接口的单例类,在main方法里先序列化再反序列化获得对象,打印两个实例的hashcode:
hashcode不同,单例又被破坏了
解决方法:在单例类里添加一个readResolve方法:
/**
* 防止序列化反序列化破坏单例模式的方法
* @return
*/
private Object readResolve() {
return SingleInnerClass.testSingleClass2;
}
添加这个方法之后,再执行main方法,这样反序列化之后也是同样的对象了:
在网上看到一条评论比较形象:浅拷贝与深拷贝的区别
为什么加了这个readResolve之后就返回相同hashcode的对象了呢,我去翻了翻jdk源码,由于水平渣渣,只能猜个大概,我也不知道对不对。
我认为事情出在反序列化读取对象的readObject方法上:
//反序列化对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("test.txt")));
TestSingleClass2 tsc1 = (TestSingleClass2) ois.readObject();
这个readObject()方法大概是这样的:
public final Object readObject()
throws IOException, ClassNotFoundException
{
//前面看不懂,略掉
try {
Object obj = readObject0(false);
//这块儿也看不懂,略掉
return obj;
} finally {
//看不懂,略掉
}
}
然后顺藤摸瓜,去看readObject0这个方法(老外起名字也这么随便么。。。):
//我比较懒,只放这个方法里我猜测的关键部分
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
继续,去看readOrdinaryObject方法:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
//看不懂,略掉
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
//看不懂,略掉
}
return obj;
}
这里有个条件判断desc.hasReadResolveMethod(),猜测是反序列化的类里有没有readResolve方法的一个判断,然后进来if块来调用invokeReadResolve方法:
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
//看不懂,略掉
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} //后面一大堆异常处理,看不懂,不贴了
} else {
throw new UnsupportedOperationException();
}
}
readResolveMethod.invoke(obj, (Object[]) null);我猜测关键应该在这里,直接调了method.invoke方法,应该就是去调用了在单例类里实现的readResolve方法,那个方法直接返回现成的单例对象,所以维持了单例模式。
然后我想验证一下,于是打了几个断点进了debug,基本就是这个流程,invoke之后就进了单例类的readResolve方法,效果和调用getInstance是一样的,返回相同的对象,维持住了单例模式。
不知道我的猜测是不是对的,只是猜测,仅供参考。
最后,网上说,最好还是推荐用枚举类的方法创造单例类,不用搞这些乱七八糟的,就正常创建,什么反射,反序列化都不会破坏掉单例模式,反正就看起来很高端。。