线程安全的单例模式

单例模式即是为保证只有一个对象实例,通常用于全局对象管理,比如XML读写实例、系统配置实例、任务调度实例、数据库连接池。在多线程的环境下,核心问题是如何保证单例延迟初始化

饿汉式单例

按照实例初始化时机来划分,饿汉式和懒汉式两种。其他单例模式基于是由懒汉式改变而来,本质上都是为了解决延迟初始化所带来的线程安全问题。

经典饿汉式单例模式代码如下:

  1. public class HungrySingleton {
  2. private HungrySingleton() {}
  3. private static final HungrySingleton singleton = new HungrySingleton();
  4. public static HungrySingleton getInstance() {
  5. return singleton;
  6. }
  7. }

饿汉式单例的问题不在于线程安全性,而在于不能延迟初始化。在应用中,延迟资源的初始化是相当重要的特性。因此才有了懒汉式单例的出现。

简单的懒汉式单例

代码如下:

  1. public class LazySingleton {
  2. private static LazySingleton instance;
  3. private LazySingleton() {}
  4. public static LazySingleton getInstance() {
  5. if (null == instance) {
  6. instance = new LazySingleton();
  7. }
  8. return instance;
  9. }
  10. }

在不考虑线程安全性问题的前提下,很容易写出如上的单例模式。这种写法理论上存在多个线程同时进入if语句获取不同的对象实例的问题,由于赋值覆盖,后面的线程拿到的都相同实例。这种情况并不能通过简单的单元测试复现。

为了避免“多例”问题的发生,可以为方法加同步锁,变成如下形式:

  1. public static synchronized LazySingleton getInstance() {
  2. if (null == instance) {
  3. instance = new LazySingleton();
  4. }
  5. return instance;
  6. }

增加内置锁后,重复实例的情况就不存在了。但内置锁在竞争激烈的情况会升级为重量级锁,开销大、性能差,高并发或性能要求高的场景下别用。可以从单元测试中加锁与不加锁的运行耗时看出。

接下来的单例模式都是改良懒汉模式而出现的。

双重检查锁单例模式

内置锁的懒汉模式问题在锁粒度太大了,事实上,只需要锁住实例初始化过程就行了,于是就有双重检查锁单例模式。先看代码

  1. public class DoubleCheckSingleton {
  2. private static volatile DoubleCheckSingleton instance;
  3. private DoubleCheckSingleton() {}
  4. public static DoubleCheckSingleton getInstance() {
  5. if (null == instance) {
  6. synchronized (DoubleCheckSingleton.class) {
  7. if (null == instance) {
  8. instance = new DoubleCheckSingleton();
  9. }
  10. }
  11. }
  12. return instance;
  13. }
  14. }
  1. 为什么要做两次空检查呢?
    高并发下,存在多个线程通过第一个空检查的可能性,同步代码的作用就是让这些线程在此阻塞,并让其中一个线程进行初始化操作,其余线程将不会重复初始化。后续的线程都不会通过第一个空检查,避免了加锁与同步等高代价操作。
  2. 为什么要用volatile修饰单例变量呢?
    为了禁止指令重排。分配内存与初始类实例是两个独立操作,指令重排后,可能是先分配内存再初始类实例,分配内存后空检查将失效。这样顺序在较极端的情况下,会有线程获取到刚分配但没有实例信息的内存从而产生数据异常。

静态内部类的懒汉式单例

双重检查锁单例模式已经可满足高性能、线程安全、延迟初始化这三个特性了,但是写法比较繁琐,理解起来有些难度。因此有写法简单效果一样的单例模式:

  1. public class InnerClassSingleton {
  2. private static class LazyHolder {
  3. private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
  4. }
  5. private InnerClassSingleton() {}
  6. public static synchronized InnerClassSingleton getInstance() {
  7. return LazyHolder.INSTANCE;
  8. }
  9. }

这种写法主要是应用了JVM的特性:

  1. 内部类在调用时初始化
  2. 类的初始化是线程安全的

写法简洁也很巧妙。但由于语言特性问题,还可以再演变一次。

在Java中,反射可以破坏上述实现的单例。

内部枚举类实现的单例

  1. public class InnerEnumSingleton {
  2. private InnerEnumSingleton() {
  3. }
  4. public static InnerEnumSingleton getInstance() {
  5. return SingletonEnum.Instance.singleton;
  6. }
  7. private enum SingletonEnum {
  8. Instance;
  9. private InnerEnumSingleton singleton;
  10. SingletonEnum() {
  11. this.singleton = new InnerEnumSingleton();
  12. }
  13. }
  14. }

这种方法主要应用:Java虚拟机会保证枚举类型不能被反射并且构造函数只被执行一次的特点,可以防止反射等手段破坏单例,越来越多的人推荐使用这种方法。