深入理解《单例模式》之源码分析
一、静态内部类
public class InnerClassSingleton implements Serializable { //无参构造函数 private InnerClassSingleton(){}; public static final InnerClassSingleton getInstance(){ return InnerClassHelper.INSTANCE; } //内部类 private static class InnerClassHelper{ private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); } }
它的原理是利用了类加载机制。
1.1、但是它可以被反射破坏
Class clazz = InnerClassSingleton.class; Constructor c = clazz.getDeclaredConstructor(null); c.setAccessible(true); Object o1 = c.newInstance(); Object o2 = InnerClassSingleton.getInstance();
执行这段代码会发现o1<>o2,这就破坏了单例。
为什么呢?罪魁祸首就是如下代码,它是反射的newInstance()的底层实现。
UnsafeFieldAccessorImpl.unsafe.allocateInstance(class)
我们知道new创建对象时会被编译成3条指令:
- 根据类型分配一块内存区域
- 把第一条指令返回的内存地址压入操作数栈顶
- 调用类的构造函数
而Unsafe.allocateInstance()方法值做了第一步和第二步,即分配内存空间,返回内存地址,没有做第三步调用构造函数。所以Unsafe.allocateInstance()方法创建的对象都是只有初始值,没有默认值也没有构造函数设置的值,因为它完全没有使用new机制,绕过了构造函数直接操作内存创建了对象,而单例是通过私有化构造函数来保证的,这就使得单例失败。
1.2、还可以被反序列化破坏
InnerClassSingleton o1 = null; InnerClassSingleton o2 = InnerClassSingleton.getInstance(); FileOutputStream fos = new FileOutputStream("InnerClassSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(o2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("InnerClassSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); o1 = (InnerClassSingleton) ois.readObject(); ois.close(); System.out.println(o1); System.out.println(o2);
执行完这段代码我们又会发现o1<>o2,可见通过反序列化,成功破坏了单例,创建了2个对象。
那么如何避免这种情况发生呢?很简单,只要在代码中添加:
public class InnerClassSingleton implements Serializable { ....省略重复代码 private Object readResolve(){ return InnerClassHelper.INSTANCE; } }
这时候我们可以再执行一下上面反序列化的方法,会很神奇的发现o1==o2,那这是为什么呢?我们一起来看下ois.readObject()的源码:
private Object readObject0(boolean unshared) throws IOException { ...省略 case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); } ------------------------------------------------------------------- private Object readOrdinaryObject(boolean unshared){ 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 { //重点!!! //首先isInstantiable()判断是否可以初始化 //如果为true,则调用newInstance()方法创建对象,这时创建的对象是不走构造函数的,是一个新的对象 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); //重点!!! //hasReadResolveMethod()会去判断,我们的InnerClassSingleton对象中是否有readResolve()方法 if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { //如果为true,则执行readResolve()方法,而我们在自己的readResolve()方法中 直接retrun InnerClassHelper.INSTANCE,所以还是返回的同一个对象,保证了单例 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; }
最后总结一下静态内部类写法:
优点:不用synchronized,性能好;简单
缺点:无法避免被反射、反序列化破坏
二、枚举
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; } }
反编译这段代码,得到:
static { INSTANCE = new EnumSingleton("INSTANCE",0); $VALUE = (new EnumSingleton[] { INSTANCE }); }
显然这是一种饿汉式的写法,用static代码块来保证单例(在类加载的时候就初始化了)。
2.1、可以避免被反射破坏
//反射 Class clazz = EnumSingleton.class; //拿到构造函数 Constructor c = clazz.getDeclaredConstructor(String.class, int.class); c.setAccessible(true); EnumSingleton instance1 = (EnumSingleton)c.newInstance("smart", 111); ----------------------------------------------------------------------------------------- public T newInstance(Object ... initargs){ if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); }
可以看到,在newInstance()方法中,做了类型判断,如果是枚举类型,直接抛出异常。也就是说从jdk层面保证了枚举不能被反射。
2.2、可以避免被反序列化破坏
Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
...省略 EnumSingleton o1 = (EnumSingleton) ois.readObject(); ----------------------------------------------------------------------------------- private Object readObject0(boolean unshared) throws IOException { ...省略 case TC_ENUM: return checkResolve(readEnum(unshared)); } ------------------------------------------------------------------- private Object readEnum(boolean unshared){ ...省略 String name = readString(false); Enum<?> result = null; Class<?> cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") //重点!!! //通过valueOf方法获取Enum,参数为class和name 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); } } }
所以序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
三、ThreadLocal单例模式
public class Singleton { private Singleton(){} private static final ThreadLocal<Singleton> threadLocal = new ThreadLocal<Singleton>(){ @Override protected Singleton initialValue(){ return new Singleton(); } }; public static Singleton getInstance(){ return threadLocal.get(); } }
这种写法利用了ThreadLocal的特性,可以保证局部单例,即在各自的线程中是单例的,但是线程与线程之间不保证单例。
应用场景(在Spring的第三方包baomidou的多数据源中,有用到这种写法):
package com.baomidou.dynamic.datasource.toolkit; import java.util.concurrent.LinkedBlockingDeque; public final class DynamicDataSourceContextHolder { //重点!!! private static final ThreadLocal<LinkedBlockingDeque<String>> LOOKUP_KEY_HOLDER = new ThreadLocal() { protected Object initialValue() { return new LinkedBlockingDeque(); } private DynamicDataSourceContextHolder() { } public static String getDataSourceLookupKey() { LinkedBlockingDeque<String> deque = (LinkedBlockingDeque)LOOKUP_KEY_HOLDER.get(); return deque.isEmpty() ? null : (String)deque.getFirst(); } public static void setDataSourceLookupKey(String dataSourceLookupKey) { ((LinkedBlockingDeque)LOOKUP_KEY_HOLDER.get()).addFirst(dataSourceLookupKey); } public static void clearDataSourceLookupKey() { LinkedBlockingDeque<String> deque = (LinkedBlockingDeque)LOOKUP_KEY_HOLDER.get(); if (deque.isEmpty()) { LOOKUP_KEY_HOLDER.remove(); } else { deque.pollFirst(); } } }; }
PS:initialValue()一般是用来在使用时进行重写的,如果在没有set的时候就调用get,会调用initialValue方法初始化内容。
读者福利
分享免费学习资料
针对于Java程序员,我这边准备免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)
为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!希望读到这的您能点个小赞和关注下我,以后还会更新技术干货,谢谢您的支持!
资料领取方式:加入Java技术交流群963944895
,点击加入群聊,私信管理员即可免费领取