6 注册式单例模式
将每一个实例都缓存到统一的容器中,使用唯一表示获取实例。
注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
6.1 枚举式单例模式
先来看枚举式单例模式的写法,来看代码,创建EnumSingleton类:
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;} }
来看测试代码:
public class EnumSingletonTest { public static void main(String[] args) { try { EnumSingleton instance1 = null; EnumSingleton instance2 = EnumSingleton.getInstance(); instance2.setData(new Object()); FileOutputStream fos = new FileOutputStream("EnumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); instance1 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(instance1.getData()); System.out.println(instance2.getData()); System.out.println(instance1.getData() == instance2.getData()); } catch (Exception e) { e.printStackTrace(); } } } java.lang.Object@2acf57e3 java.lang.Object@2acf57e3 true
没有做任何处理,我们发现运行结果和预期的一样。那么枚举式单例模式如此神奇,它的神秘之处 在哪里体现呢?下面通过分析源码来揭开它的神秘面纱。
下载一个非常好用的 Java反编译工具 Jad(下载地址:https://varaneckas.com/jad/),解压后 配置好环境变量(这里不做详细介绍),就可以使用命令行调用了。找到工程所在的Class目录,复制 EnumSingleton.class 所在的路径,如下图所示。
然后切换到命令行,切换到工程所在的Class目录,输入命令 jad 并在后面输入复制好的路径,在 Class 目录下会多出一个 EnumSingleton.jad 文件。打开 EnumSingleton.jad 文件我们惊奇地发现有 如下代码:
static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); }
原来,枚举式单例模式在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例模式的实现。至 此,我们还可以试想,序列化能否破坏枚举式单例模式呢?不妨再来看一下 JDK 源码,还是回到 ObjectInputStream的readObject0()方法:
private Object readObject0(boolean unshared) throws IOException { ... 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; }
我们发现,枚举类型其实通过类名和类对象类找到一个唯一的枚举对象。因此,枚举对象不可能被 类加载器加载多次。那么反射是否能破坏枚举式单例模式呢?来看一段测试代码:
public static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(); c.newInstance(); } catch (Exception e) { e.printStackTrace(); } }
结果中报的是 java.lang.NoSuchMethodException异常,意思是没找到无参的构造方法。这时候, 我们打开 java.lang.Enum的源码,查看它的构造方法,只有一个protected类型的构造方法,代码如 下:
protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }
我们再来做一个下面这样的测试:
public static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(String.class, int.class); c.setAccessible(true); EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("Tom", 666); } catch (Exception e) { e.printStackTrace(); } }
这时错误已经非常明显了,“Cannot reflectively create enum objects”,即不能用反射来创建 枚举类型。还是习惯性地想来看看JDK源码,进入Constructor的newInstance()方法:
@CallerSensitive 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); } } 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 枚举类型,则直接抛出异常。
到此为止,我们是不是已经非常清晰明了呢?枚举式单例模式也是《EffectiveJava》书中推荐的一种单例模式实现写法。JDK枚举的语法特殊性及反射也为枚举保驾护航,让枚举式单例模式成为一种比 较优雅的实现。
枚举源码
java.lang.Enum通过valueOf获得值
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name); } Map<String, T> enumConstantDirectory() { if (enumConstantDirectory == null) { T[] universe = getEnumConstantsShared(); if (universe == null) throw new IllegalArgumentException( getName() + " is not an enum type"); Map<String, T> m = new HashMap<>(2 * universe.length); for (T constant : universe) m.put(((Enum<?>)constant).name(), constant); enumConstantDirectory = m; } return enumConstantDirectory; } private volatile transient Map<String, T> enumConstantDirectory = null;
枚举模式的实例天然具有线程安全性,防止序列化与反射的特性。
有点像饿汉式单例。创建时就将常量存放在map容器中。
优点:写法优雅。加载时就创建对象。线程安全。
缺点:不能大批量创建对象,否则会造成浪费。spring中不能使用它。
结论:如果不是特别重的对象,建议使用枚举单例模式,它是JVM天然的单例。
6.2 容器式单例
Spring改良枚举写出的改良方法:IOC容器
接下来看注册式单例模式的另一种写法,即容器式单例模式,创建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); } } }
测试
public class ContainerSingletonTest { public static void main(String[] args) { Object instance1 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo"); Object instance2 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo"); System.out.println(instance1 == instance2); } }
结果
true
容器式单例模式适用于实例非常多的情况,便于管理。但它是非线程安全的。到此,注册式单例模式介绍完毕。我们再来看看Spring中的容器式单例模式的实现代码:
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */ private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<String, BeanWrapper>(16); }
容器为啥不能被反射破坏?秩序的维护者,创造了一个生态
7 线程单例实现ThreadLocal
最后赠送给大家一个彩蛋,讲讲线程单例实现 ThreadLocal。ThreadLocal 不能保证其创建的对象 是全局唯一的,但是能保证在单个线程中是唯一的,天生是线程安全的。下面来看代码:
public class ThreadLocalSingleton { private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance = new ThreadLocal<ThreadLocalSingleton>(){ @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; private ThreadLocalSingleton(){} public static ThreadLocalSingleton getInstance(){ return threadLocaLInstance.get(); } }
写一下测试代码:
public class ThreadLocalSingletonTest { public static void main(String[] args) { System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("End"); } }
运行结果如下图所示。
com.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 End Thread-0:com.vip.pattern.singleton.lazy.LazyDoubleCheckSingleton@551f86f1 Thread-1:com.vip.pattern.singleton.lazy.LazyDoubleCheckSingleton@551f86f1
我们发现,在主线程中无论调用多少次,获取到的实例都是同一个,都在两个子线程中分别获取到 了不同的实例。那么 ThreadLocal是如何实现这样的效果的呢?我们知道,单例模式为了达到线程安全 的目的,会给方法上锁,以时间换空间。ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。
不是线程作为key,而是threadlocal本身。
ThreadLocal源码
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
8 AbstractFactoryBean源码
AbstractFactoryBean
public final T getObject() throws Exception { if (isSingleton()) { return (this.initialized ? this.singletonInstance : getEarlySingletonInstance()); } else { return createInstance(); } } private T getEarlySingletonInstance() throws Exception { Class[] ifcs = getEarlySingletonInterfaces(); if (ifcs == null) { throw new FactoryBeanNotInitializedException( getClass().getName() + " does not support circular references"); } if (this.earlySingletonInstance == null) { this.earlySingletonInstance = (T) Proxy.newProxyInstance( this.beanClassLoader, ifcs, new EarlySingletonInvocationHandler()); } return this.earlySingletonInstance; }
MyBatis的ErrorContext使用了ThreadLocal
public class ErrorContext { private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>(); private ErrorContext() { } public static ErrorContext instance() { ErrorContext context = LOCAL.get(); if (context == null) { context = new ErrorContext(); LOCAL.set(context); } return context; } }
9 单例模式小结
单例模式优点:
- 在内存中只有一个实例,减少了内存开销。
- 可以避免资源的多重占用。
- 设置全局访问点,严格控制访问。
单例模式的缺点:
- 没有接口,扩展困难。
- 如果要扩展单例对象,只有修改代码,没有其他途径。
学习单例模式的知识重点总结
- 私有化构造器
- 保证线程安全
单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。单例模式看起来非常简单,实现起来其实也非常简单,但是在面试中却是一个高频面试点。。