为什么写了readResolve方法,反序列化就不会破坏单例呢?原因如下:
ObjectInputStream的readOrdinaryObject(boolean unshared)方法
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); } passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(passHandle, resolveEx); } if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); } handles.finish(passHandle); //重点:这里的意思就是:如果有写readResolve方法,就调用readResovle方法 if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { //反射调用ReadResolve方法 Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { handles.setObject(passHandle, obj = rep); } } return obj; }
看到上面应该很清楚了,在条件判断中 desc.hasReadResolveMethod()会判断是否有readResolve()方法,如果有的话会通过desc.invokeReadResolve(obj)去反射调用该方法,返回的就是同一个对象。
教你破坏单例模式
序列化破坏单例&反射破坏单例
通过JDK源码分析我们可以看出,虽然增加了readResolver()方法返回实例解决了单例模式别破坏的问题,但是实际上实例化了两次,只不过新创建的对象并没有被返回而已。如果创建对象的动作发生频率加快,就意味着内存分配开销也会随之增大。于是引入了 下边注册时的单例模式。
五、注册式单例模式
注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识,注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
1、枚举式单例模式
public enum EnumSingleton { INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance(){return INSTANCE;} }
枚举式单例能够防止序列化破坏单例
使用非常好用的反编译工具Jad,解压之后配置好环境变量,就可以使用命令行调用了。找到工程所在Class目录,复制EnumSingleton.class所在的路径。
然后切换命令行,切换到工程所在的Class目录,输入命令jad并在后面输入复制好的路径,在Class目录下边会多出一个EnumSingleton.jad文件。打开EnumSingleton.jad文件,我们惊奇的发现有如下代码:
staic { INSTANCE = new EnumSingleton("INSTANCE",0); $VALUES = (new EnumSingletion[]{ INSTANCE }); }
原来,枚举类单例模式在静态代码块中就给INSTANCE进行了复制,是饿汉式单例模式的实现。
至此,我们还可以试想,序列化能否破坏枚举式单例模式呢?
private Object readObject0(){ ... case TC_ENUM: return checkResolve(readEnum(unshared)); ... }
我们看到,在readObject0()中调用了readEnum()方法,来看readEnum()方法的代码实现:
private Enum<?> readEnum(boolean unshared) throws IOException { if (bin.readByte() != TC_ENUM) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); if (!desc.isEnum()) { throw new InvalidClassException("non-enum class: " + desc); } int enumHandle = handles.assign(unshared ? unsharedMarker : null); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(enumHandle, resolveEx); } String name = readString(false); Enum<?> result = null; Class<?> cl = desc.forClass(); if (cl != null) { try { //核心代码:通过类名和类对象找到一个唯一的枚举对象。 @SuppressWarnings("unchecked") Enum<?> en = Enum.valueOf((Class)cl, name); result = en; } catch (IllegalArgumentException ex) { throw (IOException) new InvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { handles.setObject(enumHandle, result); } } handles.finish(enumHandle); passHandle = enumHandle; return result; }
枚举式单例能够防止反射破坏单例
枚举式单例在初始化的时候使用静态代码块,饿汉式;在初始化实例的时候JDK中也有判断
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } //JDK中的判断,如果是枚举,就无法生成实例 if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
从上述代码可以看出,在newInstance()方法中做了强制性判断,如果修饰符Modifier.ENUM枚举类型,则直接抛出异常。这个地方和静态内部类的处理方式有异曲同工之妙。对,但是我们自己在构造方法中写逻辑处理可能存在未知的风险,而JDK的处理是最官方、最权威、最稳定的。因为枚举式单例模式也是《Effective Java》书中推荐的一种单例模式实现写法。
六、
四、静态内部类
2.容器式单例
其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么接下来看注册式 单例模式的另一种写法,即容器式单例模式,创建ContainerSingleton类
public class ContainerSingleton { private ContainerSingleton(){} private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>(); public static Object getInstance(String className){ Object instance = null; if(!ioc.containsKey(className)){ try { instance = Class.forName(className).newInstance(); ioc.put(className, instance); }catch (Exception e){ e.printStackTrace(); } return instance; }else{ return ioc.get(className); } } }
容器式单例模式适用于需要大量创建单例对象的场景,便于管理。但是它是非线程安全的。到此,
注册式单例模式介绍完毕。我们再来看看Spring中的容器式单例模式的实现代码:
六、ThreadLocal单例模式