大家好我是miHotel,今天来复习一下设计模式中的单例模式,下面是思维导图。
什么是单例模式 单例模式顾名思义,就是一个类从始至终只能创建一个对象,并且提供了一个全局访问点。ServletContext、ServletContextConfig、ApplicationContext、数据库连接池都是单例模式。
饿汉单例模式 首先是饿汉单例模式,所谓的“饿汉”就是不采用延迟加载,在类加载的时候就初始化,并创建单例对象。饿汉单例模式是绝对线程安全的,因为在线程还没有出现的时候就初始化了。 第一种写法:
1 2 3 4 5 6 7 8 9 public class HungrySingleton { private static final HungrySingleton hungrySingleton = new HungrySingleton (); private HungrySingleton () {}; public static HungrySingleton getInstance () { return hungrySingleton; } }
利用静态代码块的第二种写法:
1 2 3 4 5 6 7 8 9 10 11 12 public class HungryStaticSingleton { private static final HungryStaticSingleton hungrySingleton; static { hungrySingleton = new HungryStaticSingleton (); } private HungryStaticSingleton () {}; public static HungryStaticSingleton getInstance () { return hungrySingleton; } }
饿汉模式没有加锁,执行效率高,因而用户体验更好。但是很可能出现这个单例对象一直未被使用的情况,也就是“占坑不拉屎”浪费内存。为了解决这个问题下面来看看懒汉单例模式:
懒汉单例模式 懒汉式就是在需要使用对象的时候再去创建对象,下面就是一种写法:
1 2 3 4 5 6 7 8 9 10 public class LazySimpleSingleton { private static LazySimpleSingleton lazySimpleSingleton = null ; private LazySimpleSingleton () {} public static LazySimpleSingleton getInstance () { if (lazySimpleSingleton == null ) { lazySimpleSingleton = new LazySimpleSingleton (); } return lazySimpleSingleton; } }
这种写法存在的问题就是线程不安全 ,当多个线程同时访问getInstance方法时,有可能同时进入if语句,从而创建了多个对象,后创建的对象会覆盖第一个对象。通过加锁可以解决线程不安全问题:
1 2 3 4 5 6 7 8 9 10 public class LazySimpleSingleton { private static LazySimpleSingleton lazySimpleSingleton = null ; private LazySimpleSingleton () {} public synchronized static LazySimpleSingleton getInstance () { if (lazySimpleSingleton == null ) { lazySimpleSingleton = new LazySimpleSingleton (); } return lazySimpleSingleton; } }
给getInstance整个方法进行加锁,这样当一个线程进入该方法时另外一个线程会从RUNNING状态变成MONITOR状态,第一个线程执行完成后,第二个线程再进入getInstance方法后lazySimpleSingleton≠null,因此只会创建一个对象。这种写法虽然解决了线程安全问题,但是性能差,当有大量线程访问是会照成很多线程发生阻塞,因此需要对代码进行改进,下面是双检的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazy; private LazyDoubleCheckSingleton () {} public static LazyDoubleCheckSingleton getInstance () { if (lazy == null ) { synchronized (LazyDoubleCheckSingleton.class) { if (lazy == null ) { lazy = new LazyDoubleCheckSingleton (); } } } return lazy; } }
通过改进,多个线程都可以进入getInstance方法,也可以通过第一层检查,然后就会竞争一个锁,第一个拿到锁的线程会对lazy进行实例化,后面拿到锁的线程就不会通过第二层检查。这里给静态成员变量加上volatile关键字是防止编译器对lazy = new LazyDoubleCheckSingleton()这段代码的指令进行优化,这段代码的指令分为三条:
分配内存空间
初始化对象
将变量lazy指向刚刚分配的内存空间(这步后lazy不为null)
volatile可以防止第二条和第三条指令发生重排序,指令顺序变为1->3->2,也就是lazy不为null但是没有初始化。在 3 执行完毕、2 未执行之前,被另一个抢占了,这时 lazy已经是非 null 了(但却没有初始化),所以该线程会直接返回 lazy,然后使用,然后顺理成章地报空指针。
双检的写法毕竟是需要加锁的,所以还是会对性能产生影响。利用类的加载性质,可以使用静态内部类实现懒汉单例模式,首先我们知道一个类的初始化时机有以下6种:
(1) 创建类的实例( new)
(2) 访问某个类或接口的静态变量,或对静态变量赋值
(3) 调用类的静态方法
(4) 反射 (Class.forname(“全限定类名”))
(5) 初始化一个类的子类(先初始化父类)
(6) JVM启动时标明的启动类,就是类名和文件名相同的那个类 注意: 访问常量 (static final)不会导致类的初始化; 使用Class.loader()方法加载类时也不会对类初始化
可以看到访问某个类的静态的静态变量这个类就会被初始化,类的加载过程不会马上加载内部类,而是在使用时进行加载,利用这个性质就能实现懒加载,而JVM已经保证了线程的安全性,静态内部类在方法调用前就会被初始化,代码如下:
1 2 3 4 5 6 7 8 9 public class LazyInnerClassSingleton { private LazyInnerClassSingleton () {} public static LazyInnerClassSingleton getInstance () { return LazyHolder.LAZY; } private static class LazyHolder { private static LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton (); } }
反射破坏单例模式 上面代码的构造器除了加了private修饰符以外没有做任何限制,因此可以通过反射来破坏单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test04 () { try { Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class; Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null ); c.setAccessible(true ); LazyInnerClassSingleton o1 = c.newInstance(); LazyInnerClassSingleton o2 = c.newInstance(); System.out.println(o1 == o2); }catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } }
输出:false, 因此需要对构造器进行处理,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class LazyInnerClassSingleton2 { private LazyInnerClassSingleton2 () { if (LazyHolder.LAZY != null ) { throw new RuntimeException ("不允许创建多个实例" ); } } public static LazyInnerClassSingleton2 getInstance () { return LazyHolder.LAZY; } public static class LazyHolder { public static LazyInnerClassSingleton2 LAZY = new LazyInnerClassSingleton2 (); } }
通过反射调用构造器创建对象时,执行LazyHolder.LAZY语句时,静态内部类会初始化,导致LazyHolder.LAZY != null,从而抛出异常,无法创建多个对象。
序列化破坏单例对象 序列化是指将一个对象序列化后写入磁盘,下次使用时再从磁盘种读取对象并进行反序列化,转化为内存对象。反序列化的对象会重新分配内存,因此如果反序列化的目标对象是单例对象就会破坏单例,下面来看代码: 首先写一个饿汉单例模式:
1 2 3 4 5 6 7 8 9 10 public class SeriableSingleton implements Serializable { private static SeriableSingleton instance = new SeriableSingleton (); private SeriableSingleton () {} public static SeriableSingleton getInstance () { return instance; } }
下面我们测试反序列化是否能破坏单例模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void test06 () { try { SeriableSingleton s1 = null ; SeriableSingleton s2 = SeriableSingleton.getInstance(); FileOutputStream fos = new FileOutputStream ("SeriableSingleton.obj" ); ObjectOutputStream oos = new ObjectOutputStream (fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream ("SeriableSingleton.obj" ); ObjectInputStream ooi = new ObjectInputStream (fis); s1 = (SeriableSingleton) ooi.readObject(); System.out.println(s1); System.out.println(s2); System.out.println(s1 == s2); } catch (Exception e) { e.printStackTrace(); } }
控制台输出如图所示: 可以看到反序列化后的对象和手动创建的对象是不一样的,实例化了两次,也就破坏了单例。那么如何解决这个问题呢?其实我们只需要增加readResolve方法即可,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class SeriableSingleton implements Serializable { private static SeriableSingleton instance = new SeriableSingleton (); private SeriableSingleton () {} public static SeriableSingleton getInstance () { return instance; } private Object readResolve () { return instance; } }
再来测试一下,看看运行结果,如下图所示。 可以看到神奇的事情发生了, 加上了readResolve方法后反序列化并没有破坏单例模式,那这到底是什么原因呢,我们来看看ObjectInputStream类的readObject方法:
1 2 3 4 5 6 7 8 9 10 11 private final Object readObject (Class<?> type) throws IOException, ClassNotFoundException { .... try { Object obj = readObject0(type, false ); ..... return obj; } ....... }
再进入readObject0方法
1 2 3 4 5 6 7 8 9 private Object readObject0 (Class<?> type, boolean unshared) throws IOException { ... switch (tc) { case TC_OBJECT: .... return checkResolve(readOrdinaryObject(unshared)); } ... }
可以看到在TC_OBJECT中调用了readOrdinaryObject方法, 我们再进入该方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 private Object readOrdinaryObject (boolean unshared) throws IOException { ... 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); } ... if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); ..... if (rep != obj) { ..... handles.setObject(passHandle, obj = rep); } } ... return obj; }
通过阅读源码,我们可以知道,isInstantiable()是判断构造器是否为空,如果不为空就返回true,因此就会创建一个新的对象。再往下看我们可以看到还调用了hasReadResolveMethod方法来判断反序列化的对象有没有readResolve方法,如果有则返回true,然后就会invokeReadResolve方法,看这方法名就知道是通过反射来调用readResolve方法,我们进入该方法看看源码:
1 2 3 4 5 6 7 Object invokeReadResolve (Object obj) throws IOException, UnsupportedOperationException { .... return readResolveMethod.invoke(obj, (Object[]) null ); .... }
到这是不是有点恍然大悟,rep就是调用我们添加的readResolve方法返回的单例对象,源码中做了一个判断,如果创建的obj对象与rep对象不同,则将rep赋值给obj,最后返回,这样反序列化返回的就是单例对象。通过源码分析可以看出readResolve虽然解决了单例模式被破坏的问题,但是实际上该对象还是创建了两次,只是最后返回了相同的引用,如果创建对象的频率加快,内存开销也会增大。
注册式单例模式 注册式单例模式顾名思义就是将每个实例用唯一标识符登记再某个“本子”上,如果要使用某个实例时,就去”本子“上查。注册式单例模式可以分为枚举式和容器式。
1.枚举式单例模式
首先我们来看一下枚举式单例模式的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; } }
然后我们测试一下反序列化能否打破单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void test07 () { 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 ooi = new ObjectInputStream (fis); instance1 = (EnumSingleton) ooi.readObject(); System.out.println(instance1); System.out.println(instance2); System.out.println(instance1 == instance2); } catch (Exception e) { e.printStackTrace(); } }
运行结果如图所示
为啥枚举式单例模式不会被反序列化破坏呢,我们使用反编译工具jad来看一下EnumSingleton.class的反编译代码, 可以看到如下代码
1 2 3 4 5 6 7 static { INSTANCE = new EnumSingleton ("INSTANCE" , 0 ); $VALUES = (new EnumSingleton [] { INSTANCE }); }
这说明枚举型单例模式是饿汉模式的实现,我们再看下JDK源码,搞清楚反序列化为啥不能破坏枚举式单例模式。下面是java.io.ObjectInputStream#readObject0的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 private Object readObject0 (Class<?> type, boolean unshared) throws IOException { .... switch (tc) { .... case TC_ENUM: if (type == String.class) { throw new ClassCastException ("Cannot cast an enum to java.lang.String" ); } return checkResolve(readEnum(unshared)); .... } .... }
再进入readEnum方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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; }
可以发现,反序列化的枚举类型是通过类名和类对象来找到唯一的枚举对象,因此枚举对象不会被类加载器加载多次。再试试反射能否破坏单例模式, 首先java.lang.Enum只有一个构造器:
1 2 3 4 protected Enum (String name, int ordinal) { this .name = name; this .ordinal = ordinal; }
我们尝试通过反射来调用这个构造器,创建对象
1 2 3 4 5 6 7 8 9 10 11 @Test public void test08 () { try { Class<EnumSingleton> clazz = EnumSingleton.class; Constructor<EnumSingleton> c = clazz.getDeclaredConstructor(String.class, int .class); c.setAccessible(true ); EnumSingleton miHoltel = c.newInstance("miHoltel" , "666" ); } catch (Exception e) { e.printStackTrace(); } }
运行结果如下图所示:
错误提示不能通过反射创建枚举对象,我们再来看看JDK源码,进入Contructor类的newInstance方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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; if (ca == null ) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
可以看到在newInstance方法中做了强制判断,如果是枚举类型则直接抛出异常。
容器式单例模式 下面来看一下注册式单例模式的另外一种写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ContainerSingleton { private ContainerSingleton () {} private static Map<String, Object> ioc = new ConcurrentHashMap <>(); public static Object getBean (String className) { synchronized (ioc) { if (!ioc.containsKey(className)) { Object obj = null ; try { obj = Class.forName(className).newInstance(); ioc.put(className, obj); } catch (Exception e) { e.printStackTrace(); } return obj; } else { return ioc.get(className); } } } }
容器式单例模式适用于实例非常多的情况。Spring中应用容器式单例模式的代码如下:
1 2 3 4 5 6 7 8 9 10 11 public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { ..... private final ConcurrentMap<String, BeanWrapper> factoryBeanInstanceCache; public AbstractAutowireCapableBeanFactory () { ... this .factoryMethodCandidateCache = new ConcurrentHashMap (); ... } ..... }