单例模式详解

单例模式详解 1.1单例模式概述

单例模式(Singleton Pattern)指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,属于创建型设计模式。
1.2单例模式的应用场景
单例模式可以保证JVM中只存在单一实例,应用场景主要有以下几个方面:
  • 需要频繁创建一些类的对象,使用单例模式可以降低系统的内存压力,减少GC。
  • 一些类创建实例的过程复杂且占用资源过多,或耗时较长,并且经常使用。
  • 系统上需要单一控制逻辑的操作。
1.3单例模式的写法分类 1.3.1“饿汉式”单例写法
public class HungrySingleton { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { }public static HungrySingleton getInstance(){ return instance; } }

上边是饿汉式单例的标准写法,可以看到饿汉式这种方式会在类加载的时候立刻初始化,并且创建单例对象,它是绝对的线程安全,因为在线程还没有访问的时候已经实例化结束,不可能存在访问安全的问题。
  • 饿汉式的另一种写法
public class HungrySingleton { private static final HungrySingleton instance; static { instance = new HungrySingleton(); }private HungrySingleton(){}public static HungrySingleton getInstance(){ return instance; } }

这种写法采用静态代码块的机制。
饿汉式单例优缺点分析 饿汉式单例的写法适用于单例对象较少的情况,这样写可以保证绝对的线程安全,执行效率比较高。但是缺点也很明显,饿汉式会在类加载的时候就将所有单例对象实例化,这样系统中如果有大量的饿汉式单例对象的存在,系统初始化的时候会造成大量的内存浪费,从而导致系统的内存不可控,换句话说就是不管对象用不用,对象都已存在,占用内存。
为了解决饿汉式写法带来内存浪费的问题,引出了懒汉式写法。
1.3.2懒汉式单例写法
  • 特点:单例对象在被使用的时候才会进行实例化
public class LazySingleton { private LazySingleton(){}private static LazySingleton instance; public static LazySingleton getInstance(){ if (instance == null){ instance = new LazySingleton(); } return instance; } }

上边是懒汉式单例的标准写法,从代码可以看出单例对象只有在被使用的时候才会进行实例化,但是这种方式又会引入一个新的问题。在多线程的环境下执行,存在线程安全问题,可能破坏单例。
验证一下:
public class ExcutorThread implements Runnable{ @Override public void run() { LazySingleton instance = LazySingleton.getInstance(); System.out.println(Thread.currentThread().getName()+":"+instance); } } public class LazySingletonTest { public static void main(String[] args) { Thread thread1 = new Thread(new ExcutorThread()); Thread thread2 = new Thread(new ExcutorThread()); thread1.start(); thread2.start(); System.out.println("end"); } }

由于我们这里只有两条线程模拟多线程执行,需进行多次才能获取破坏单例情况,以下为捕捉到的单例被破坏情况:
单例模式详解
文章图片

那么我们如何解决线程不安全呢?第一个应该想到的就是synchronized关键字:
public class LazySingleton { private LazySingleton(){}private static LazySingleton instance; public static synchronized LazySingleton getInstance(){ if (instance == null){ instance = new LazySingleton(); } return instance; } }

这样我们就解决了线程安全问题,虽然目前解决了线程安全问题,但是当调用getInstance方法的线程很多,只有一个线程RUNNING,其他线程会出现阻塞,又引入了性能问题,我们来进一步优化。
  • 双重检锁方式(Double Check)
public class LazySingleton { private LazySingleton(){}private volatile static LazySingleton instance; public static LazySingleton getInstance(){ // 是否会进行阻塞 if (instance == null){ synchronized (LazySingleton.class){ // 是否需要创建实例 if (instance == null){ instance = new LazySingleton(); } } } return instance; } }

减少锁的碰撞几率,并且将锁放置到getInstance内部,调用者感受不明显。
但是只要有锁就会对性能有一定影响,这时我们从类初始化的角度考虑,引出静态内部类的方式。
  • 静态内部类写法
public class LazySingletonInnerClass { private LazySingletonInnerClass(){}public static LazySingletonInnerClass getInstance(){ return LazyHolder.INSTANCE; }private static class LazyHolder{ private static final LazySingletonInnerClass INSTANCE = new LazySingletonInnerClass(); } }

只有在调用getInstance将内部类加载,实现懒加载,内部类只加载一次,线程安全。
思考:上边这种方式就真的完美了吗?我们所有的单例模式中构造方法私有化,我们一直在获取实例的方法下功夫,而JAVA提供的反射是完全可以破坏私有化构造方法的,接下来尝试用反射破坏:
public static void main(String[] args) { try{ Class clazz = LazySingletonInnerClass.class; Constructor declaredConstructor = clazz.getDeclaredConstructor(null); // 开启强制访问 declaredConstructor.setAccessible(true); // 获取两个实例破坏单例 Object instance1 = declaredConstructor.newInstance(); Object instance2 = declaredConstructor.newInstance(); }catch (Exception e){ e.printStackTrace(); } }

这个时候构造方法被调用,破坏了单例,那我们彻底不让调用构造方法,调用构造方法时候进行抛异常,继续优化。
public class LazySingletonInnerClass { private LazySingletonInnerClass(){ throw new RuntimeException("不允许调用构造方法"); }public static LazySingletonInnerClass getInstance(){ return LazyHolder.INSTANCE; }private static class LazyHolder{ private static final LazySingletonInnerClass INSTANCE = new LazySingletonInnerClass(); } }

但是在构造方法中抛出异常,着实不妥,但是JAVA又又天然不允许反射调用构造方法的类---枚举,引出枚举单例模式
  • 枚举单例模式
public enum EnumSingleton { INSTANCE; public static EnumSingleton getInstance(){ return INSTANCE; } }

验证一下:
public static void main(String[] args) { try{ Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(String.class, int.class); c.setAccessible(true); EnumSingleton instance =(EnumSingleton) c.newInstance("123", 12); System.out.println(instance); }catch (Exception e){ e.printStackTrace(); } }

运行结果:
单例模式详解
文章图片

真相大白,枚举不可用反射破坏构造方法,JDK的处理方式是最权威的,JDK枚举的特殊性,让代码实现更优雅。
至此,我们已经分析了各种创建单例对象时候出现的问题以及解决办法,但是创建完单例对象之后,有时候我们会使用序列化将对象写入磁盘,当下次使用时再从磁盘中反序列化转化为内存对象,这样也会破坏单例模式。
验证一下:
public static void main(String[] args) { HungrySingleton s1 = null; HungrySingleton s2 = HungrySingleton.getInstance(); try{ // 序列化 FileOutputStream fileOutputStream = new FileOutputStream("s2.obj"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(s2); objectOutputStream.flush(); objectOutputStream.close(); // 反序列化 FileInputStream fileInputStream = new FileInputStream("s2.obj"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); s1 = (HungrySingleton) objectInputStream.readObject(); objectInputStream.close(); System.out.println(s1); System.out.println(s2); System.out.println(s1 == s2); }catch (Exception e){ e.printStackTrace(); } }

结果破坏了单例,见下图:
单例模式详解
文章图片

那么如何保证在序列化的情况下保证单例呢?很简单,只需要增加readResolve方法。
public class HungrySingleton implements Serializable { private static final HungrySingleton instance; static { instance = new HungrySingleton(); }private HungrySingleton(){}public static HungrySingleton getInstance(){ return instance; } private Object readResolve(){ return instance; } }

单例模式详解
文章图片

这个原因的分析,因为在反序列话过程中创建出了个新对象,所以需要看下readObject源码中是如何实现的。
private final Object readObject(Class type) throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); }if (! (type == Object.class || type == String.class)) throw new AssertionError("internal error"); // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { Object obj = readObject0(type, false); // 关键方法进行标记 handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null) { throw ex; } if (depth == 0) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0) { clear(); } } }

private Object readObject0(Class type, boolean unshared) throws IOException { ...... case TC_OBJECT: if (type == String.class) { throw new ClassCastException("Cannot cast an object to java.lang.String"); } return checkResolve(readOrdinaryObject(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); if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod())// 关键方法标记 { 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; }

/**Returns true if represented class is serializable/externalizable and can be instantiated by the serialization runtime--i.e., if it is externalizable and defines a public no-arg constructor, or if it is non-externalizable and its first non-serializable superclass defines an accessible no-arg constructor. Otherwise, returns false.**/ boolean isInstantiable() { requireInitialized(); return (cons != null); }

上边这个判断的意思是如果这个类是实现了serializable/externalizable,并且可以由序列化运行时实例化,则返回true,这个时候obj = desc.newInstance 就会创建一个新的对象,所以这个时候单例就被破坏了。
此时我们在往下边看,desc.hasReadResolveMethod()这个方法:
/** * Returns true if represented class is serializable or externalizable and * defines a conformant readResolve method.Otherwise, returns false. */ boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }

根据注释说的意思是如果我们加上这个readResolve()方法,判断结果就是true,会进入if块,在看if中Object rep = desc.invokeReadResolve(obj); 方法:
/** * Invokes the readResolve method of the represented serializable class and * returns the result.Throws UnsupportedOperationException if this class * descriptor is not associated with a class, or if the class is * non-serializable or does not define readResolve. */ Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException { requireInitialized(); if (readResolveMethod != null) { try { return readResolveMethod.invoke(obj, (Object[]) null); } catch (InvocationTargetException ex) { Throwable th = ex.getTargetException(); if (th instanceof ObjectStreamException) { throw (ObjectStreamException) th; } else { throwMiscException(th); throw new InternalError(th); // never reached } } catch (IllegalAccessException ex) { // should not occur, as access checks have been suppressed throw new InternalError(ex); } } else { throw new UnsupportedOperationException(); } }

这个方法可以看出,利用反射去执行我们类中的readResolve()方法,这块readResolveMethod的赋值,可以再ObjectStreamClass类中:
private ObjectStreamClass(final Class cl) { this.cl = cl; name = cl.getName(); isProxy = Proxy.isProxyClass(cl); isEnum = Enum.class.isAssignableFrom(cl); serializable = Serializable.class.isAssignableFrom(cl); externalizable = Externalizable.class.isAssignableFrom(cl); Class superCl = cl.getSuperclass(); superDesc = (superCl != null) ? lookup(superCl, false) : null; localDesc = this; if (serializable) { AccessController.doPrivileged(new PrivilegedAction() { public Void run() { if (isEnum) { suid = Long.valueOf(0); fields = NO_FIELDS; return null; } if (cl.isArray()) { fields = NO_FIELDS; return null; }suid = getDeclaredSUID(cl); try { fields = getSerialFields(cl); computeFieldOffsets(); } catch (InvalidClassException e) { serializeEx = deserializeEx = new ExceptionInfo(e.classname, e.getMessage()); fields = NO_FIELDS; }if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); writeObjectMethod = getPrivateMethod(cl, "writeObject", new Class[] { ObjectOutputStream.class }, Void.TYPE); readObjectMethod = getPrivateMethod(cl, "readObject", new Class[] { ObjectInputStream.class }, Void.TYPE); readObjectNoDataMethod = getPrivateMethod( cl, "readObjectNoData", null, Void.TYPE); hasWriteObjectData = https://www.it610.com/article/(writeObjectMethod != null); } domains = getProtectionDomains(cons, cl); writeReplaceMethod = getInheritableMethod( cl,"writeReplace", null, Object.class); readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class); //关键方法标记 return null; } }); } else { suid = Long.valueOf(0); fields = NO_FIELDS; }try { fieldRefl = getReflector(fields, this); } catch (InvalidClassException ex) { // field mismatches impossible when matching local fields vs. self throw new InternalError(ex); }if (deserializeEx == null) { if (isEnum) { deserializeEx = new ExceptionInfo(name, "enum type"); } else if (cons == null) { deserializeEx = new ExceptionInfo(name, "no valid constructor"); } } for (int i = 0; i < fields.length; i++) { if (fields[i].getField() == null) { defaultSerializeEx = new ExceptionInfo( name, "unmatched serializable field(s) declared"); } } initialized = true; }

这个方法已经约定方法名称readResolve,这时候执行类中的readResolve方法,直接返回已经创建的实例,所以反序列化后的结果变成了在单例类中已创建的实例对象。
1.4 单例模式的扩展 1.4.1 ioc容器的启蒙
  • 容器式单例的写法
/** * Created by yml */ public class ContainerSingleton { private ContainerSingleton(){}private static Map ioc = new ConcurrentHashMap<>(); public static Object getBean(String className){ 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中存储单例对象的容器就是一个Map。
1.4.2 ThreadLocal单例详解
  • ThreadLocal单例写法
package org.example.Singleton; /** * Created by yml */ public class ThreadLocalSingleton { private ThreadLocalSingleton(){}private static final ThreadLocal threadInstance = new ThreadLocal(){ @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; public static ThreadLocalSingleton getInstance(){ return threadInstance.get(); } }

【单例模式详解】ThreadLocal的单例实现不能保证创建的单例全局唯一,但是可以保证单个线程内部是唯一的,所以是线程安全的。之前保证单例线程安全的方法是加锁,这种是以时间换空间的方式,将其他线程加锁,ThreadLocal是将对象保存到ThreadLocalMap中,以空间换时间保证线程安全。

    推荐阅读