单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建性模式。单例模式在开发中应用非常广泛,例如,Spring 框架中的 ApplicationContext、数据库的连接池等。
单例模式有如下实现方式:
饿汉式
懒汉式
注册式
单例模式在实现的时候需要考虑如下两个问题:
多线程环境下对单例模式的破坏
反射、序列化对单例模式的破坏
饿汉式单例模式
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。
Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例模式。
优点:没有加任何的锁、执行效率比较高,用户体验比懒汉式单例模式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能“占着茅坑不拉屎”。
懒汉式单例模式的写法如下所示:
public class HungrySingleton {// 1.私有化构造器private HungrySingleton (){}// 2.在类的内部创建自行实例private static final HungrySingleton instance = new HungrySingleton();// 3.提供获取唯一实例的方法(全局访问点)public static HungrySingleton getInstance(){return instance;}}
还有另外一种写法,利用静态代码块的机制:
public class HungryStaticSingleton {// 1. 私有化构造器private HungryStaticSingleton(){}// 2. 实例变量private static final HungryStaticSingleton instance;// 3. 在静态代码块中实例化static {instance = new HungryStaticSingleton();}// 4. 提供获取实例方法public static HungryStaticSingleton getInstance(){return instance;}}
这两种写法都非常的简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。下面我们来看性能更优的写法。
懒汉式单例模式
懒汉式单例模式的特点是:被外部类调用的时候内部类才会加载。
简单懒汉式(线程不安全)
下面来看懒汉式单例模式的简单实现 LazySimpleSingleton:
public class LazySimpleSingleton {private LazySimpleSingleton() {}private static LazySimpleSingleton instance = null;public static LazySimpleSingleton getInstance() {if (instance == null) {instance = new LazySimpleSingleton();}return instance;}}
然后写一个线程类 ExectorThread:
public class ExectorThread implements Runnable {@Overridepublic void run() {LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();System.out.println(Thread.currentThread().getName() + ":" + singleton);}}
客户端测试代码如下:
public class LazySimpleSingletonTest {public static void main(String[] args) {Thread t1 = new Thread(new ExectorThread());Thread t2 = new Thread(new ExectorThread());t1.start();t2.start();System.out.println("End");}}
运行结果如下图所示。

上面的代码有一定概率出现两种不同结果,这意味着上面的单例存在线程安全隐患。我们通过过时运行再具体看一下。这里教大家一种新技能,用线程模式调试,手动控制线程的执行顺序来跟踪内存的变化。先给 LazySimpleSingleton 类打上断点,如下图所示:

然后鼠标右键单击断点,切换为 Thread 模式,如下图所示。

开始“Debug”之后,会看到 Debug 控制台可以自由切换 Thread 的运行状态,如下图所示。

分别选择 Thread-0 和 Thread-1,都执行一步,都进入到 if 判断中。

这样 LazySimpleSingleton 就被实例化了两次。
简单懒汉式(线程安全)
通过对上面简单懒汉式单例的测试,我们知道存在线程安全隐患,那么,如何来避免或者解决呢?
通过给 getInstance() 方法加上 synchronized 关键字,使这个方法编程线程同步方法:
public class LazySimpleSyncSingleton {private LazySimpleSyncSingleton() {}private static LazySimpleSyncSingleton instance = null;public synchronized static LazySimpleSyncSingleton getInstance() {if (instance == null) {instance = new LazySimpleSyncSingleton();}return instance;}}
我们再来调试。当执行其中一个线程并调用 getInstance() 方法时,另一个线程在调用 getInstance() 方法,线程的状态有 RUNNING 变成了 MONITOR,出现阻塞。知道第一个线程执行完,第二个线程才恢复到 RUNNING 状态继续调用 getInstance() 方法,如下图所示。

上图完美地展现了 synchronized 监视锁的运行状态,线程安全的问题解决了。但是synchronized 加锁时,在线程数量比较多的情况下,如果 CPU 分配压力上升,则会导致大批线程阻塞,从而导致程序性能大幅下降。
双重检查锁懒汉式
那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式:
public class LazyDoubleCheckSingleton {private LazyDoubleCheckSingleton() {}private static LazyDoubleCheckSingleton instance = null;public static LazyDoubleCheckSingleton getInstance() {if (instance == null) {synchronized (LazyDoubleCheckSingleton.class) {if (instance == null) {instance = new LazyDoubleCheckSingleton();// error}}}return instance;}}
上面代码的执行顺序如下:
- 检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回;
- 获取锁;
- 再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象;
执行双重检查是因为,如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。这样,除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题。
上述写法看似解决了问题,但是有个很大的隐患。实例化对象的那行代码(标记为 error 的那行),字节码操作可以分解成以下三个步骤:
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
但是有些编译器为了考虑性能,可能会对字节码命令重排序,将第二步和第三步进行重排序,顺序就成了:
- 分配内存空间
- 将对象指向刚分配的内存空间
- 初始化对象
现在考虑重排序后,两个线程发生了以下情况的调用:
| Time | Thread A | Thread B |
|---|---|---|
| T1 | 检查到 lazyThree 为空 | |
| T2 | 获取锁 | |
| T3 | 再次检查到 lazyThree 为空 | |
| T4 | 为 lazyThree 分配内存空间 | |
| T5 | 将 lazyThree 指向内存空间 | |
| T6 | 检查到 lazyThree 不为空 | |
| T7 | 访问 lazyThree(此时对象还未完成初始化) | |
| T8 | 初始化 lazyThree |
在这种情况下,T7 时刻线程B 对 lazyThree 的访问,访问的是一个初始化未完成的对象。
正确的双重检查锁的写法如下所示:
public class LazyDoubleCheckSingleton {private LazyDoubleCheckSingleton() {}private volatile static LazyDoubleCheckSingleton instance = null;public static LazyDoubleCheckSingleton getInstance() {if (instance == null) {synchronized (LazyDoubleCheckSingleton.class) {if (instance == null) {instance = new LazyDoubleCheckSingleton();}}}return instance;}}
为了解决上述问题,需要在 instance 前加入关键字 volatile。使用了 volatile 关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。
静态内部类懒汉式
用到 synchronized 关键字总归要上锁,对程序性能还是存在一定的影响。有没有更好的方案?当然有,我们可以从类初始化的角度来考虑,看下面的代码,采用静态内部类的方式:
public class LazyInnerClassSingleton {private LazyInnerClassSingleton() {}// 注意关键字final,保证方法不被重写和重载public static final LazyInnerClassSingleton getInstance() {// 在返回结果之前,会先加载内部类return LazyHolder.INSTANCE;}// 默认不加载private static class LazyHolder {private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();}}
这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。
我们可以通过如下时序图来看一下调用顺序:

- 客户端调用 LazyInnerClassSingleton.getInstance(),此时会先判断 LazyInnerClassSingleton 这个类是否已经加载,如果没有加载则先加载,然后调用 getInstance 方法;
- getInstance 方法内调用了 LazyHolder.LAZY,则此时会先判断 LazyHolder 这个类是否已经加载,如果没有加载则先加载,并初始化自身的静态属性,此时 LAZY 通过 new LazyInnerClassSingleton() 完成了初始化;
- 返回 LazyHolder 的属性 LAZY 的引用,最终把引用返回到客户端;
从上面的流程逻辑,我们可以看到,内部类是在方法调用之前初始化,如果在 getInstance 方法中没有调用LazyHolder.LAZY,那么 LazyHolder 是不会完成初始化的,巧妙地避免了线程安全问题,同时节省了系统的开销。
注册式单例模式
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为枚举式单例模式,另一种为容器式单例模式。
枚举式单例模式
创建枚举类 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) {EnumSingleton instance1 = null;EnumSingleton instance2 = EnumSingleton.getInstance();instance2.setData(new Object());try {//序列化FileOutputStream fos = new FileOutputStream("EnumSingletonTest.obj");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(instance2);oos.flush();oos.close();//反序列化FileInputStream fis = new FileInputStream("EnumSingletonTest.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 反编译工具 XJad 打开 EnumSingleton.class 文件,看到有如下代码:
static{INSTANCE = new EnumSingleton("INSTANCE", 0);$VALUES = (new EnumSingleton[] {INSTANCE});}
原来,枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例模式的实现。序列化能不能破坏枚举式单例其实在 JDK 源码中也有体现,我们继续回到 ObjectInputStream 的 readObject0() 方法:
private Object readObject0(boolean unshared) throws IOException {boolean oldMode = bin.getBlockDataMode();...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;}
我们发现,枚举乐行其实通过类名和 class 对象找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。那么反射是否能破坏枚举式单例模式呢?来看一段测试代码:
private static void reflectionTest() {try {Class clazz = EnumSingleton.class;Constructor constructor = clazz.getDeclaredConstructor();EnumSingleton singleton = (EnumSingleton) constructor.newInstance();System.out.println(singleton);} catch (Exception e) {e.printStackTrace();}}
运行结果如下图所示。

结果中报的是 java.lang.NoSuchMethodException 异常,意思是没找到无参的构造方法。我们打开java.lang.Enum 的源码,发现只有一个 protected 的构造方法,代码如下:
protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;}
我们再来做一个下面这样的测试:
private static void reflectionTest() {try {Class clazz = EnumSingleton.class;Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);constructor.setAccessible(true);EnumSingleton singleton = (EnumSingleton) constructor.newInstance("tom", "666");System.out.println(singleton);} catch (Exception e) {e.printStackTrace();}}
运行结果如下图所示。

结果中报错 Cannot reflectively create enum objects,意思是不能通过反射来创建枚举类,关于这个在 JDK 源码中也有说明,我们来看 Constructor 的 newInstance() 方法:
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 volatileif (ca == null) {ca = acquireConstructorAccessor();}@SuppressWarnings("unchecked")T inst = (T) ca.newInstance(initargs);return inst;}
从上述代码可以看出,在 newInstance() 方法中做了强制性的判断,如果修饰符是 Modifier.ENUM,则直接抛出异常。
枚举式单例也是《Effective Java》书中推荐的一种单例实现写法。JDK 枚举的语法特殊性及反射也为枚举保驾护航,让枚举式单例模式成为一种比较优雅的实现。
容器式单例模式
接下来看注册式单例模式的另一种写法,即容器式单例模式,创建 ContainerSingleton 类:
public class ContainerSingleton {// 私有的构造方法private ContainerSingleton(){}// 存储实例的map,ConcurrentHashMap中线程安全,spring框架的IOC注册中心就是用这种方式实现的private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();public static Object getBean(String className){synchronized (ioc){//如果map中没有这个class实例if(!ioc.containsKey(className)){Object obj = null;try {obj = Class.forName(className).newInstance();ioc.put(className, obj);} catch (Exception e) {e.printStackTrace();}return obj;}elsereturn ioc.get(className);}}}
容器式单例模式适用于实例非常多的情况,便于管理。
线程单例实现 ThreadLocal
线程单例使用 ThreadLocal 来实现。ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。下面来看代码:
public class ThreadLocalSigleton {private static final ThreadLocal<ThreadLocalSigleton> threadLocalInstance = new ThreadLocal<ThreadLocalSigleton>(){@Overrideprotected ThreadLocalSigleton initialValue() {return new ThreadLocalSigleton();}};private ThreadLocalSigleton(){};public static ThreadLocalSigleton getInstance(){return threadLocalInstance.get();}}
测试代码:

我们发现,在主线程中无论调用多少次,获得到的实例都是同一个;在多线程环境下,每个线程获取到了不同的实例。
单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLocal 将所有的对象放在 ThreadLocalMap 中,为每个线程都提供一个对象,这实际上是以空间换时间来实现线程间隔离的。
摘录:《Spring 5 核心原理与30个类手写实战》来自文艺界的Tom老师的书籍。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/gl0ge2 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
