单例模式(Singleton)

260 阅读5分钟

一、模式介绍

单例模式是应用最广的模式之一,在应用这个模式时,单例对象的类必须保证只有一个实例存在。很多时候,整个系统只需要拥有一个全局对象,这样更有利于我们协调系统整体的行为。 例如,在一个应用中,应该只有一个ImageLoader对象,由于ImageLoader对象中含有线程池、缓存系统、网络请求等,是很消耗资源的。因此,我们没有必要构造多个实例对象,这个时候可以考虑使用单例模式。

二、模式定义

确保一个类只有一个实例对象,而且需要自行实例化并且向整个系统提供这个实例。

三、使用场景

当产生多个对象会消耗过多资源,或者某种类型的对象只应该有且只有一个时,就可以考虑使用单例模式。

四、实现方式

实现单例模式主要有以下几个关键点: (1)构造函数不对外开放,一般为Private; (2)通过一个静态方法或者枚举返回单例类对象; (3)确保单例类的对象有且只有一个,尤其是在多线程环境下; (4)确保单例类对象在反序列时不会重新构建对象。

单例模式的实现主要有以下几种方式:

1、懒汉模式

懒汉模式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化。实现如下:

public class Singleton {
  private static Singleton instance;
  private Single() {}
  
  public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
  }
}

getInstance()方法中添加了synchronized关键字,表明它是一个同步方法,这可以保证单例对象在多线程下的唯一性。但是,这有一个问题,即使instance已经被初始化,每次调用getInstance方法都会进行同步,这样会消耗不必要的资源,这也是懒汉模式存在的最大问题。

懒汉模式的优点是:单例在使用的时候才会被实例化,在一定程度上节约了资源。 懒汉模式的缺点是:第一次加载时需要及时进行实例化,反应稍慢。 最大的问题是:每次调用getInstance都进行同步,造成不必要的同步开销。 由于懒汉模式存在上述问题,故一般不建议使用。

二、Double Check Lock(DCL)模式

DCL方式实现单例模式的优点是既能够在需要时才初始化单例,又能能够保证线程安全,而且单例对象初始化后调用getInstance不进行同步锁。实现如下:

public class Singleton {
  private static Singleton instance;
  private Singleton() {
  }

  public static Singleton getInstance() {
    if(instance == null) {
        synchronized(Singleton.class) {
          if(instance == null) {
              instance  = new Singleton();
          }
        }
    }
    return instance;
  }
}

该程序的亮点都在getInstance方法上,可以看到getInstance方法中对Instance进行了两次判空: 第一次判断是为了避免不必要的同步。当instance不为空时,则直接返回,不需要进行不必要的同步。 第二次判断是为了在实例为null的情况才创建实例对象。这是什么意思呢??? 假设线程A执行到了instance = new Singleton()语句,这里看起来是一句代码,但实际上它并不是原子操作,这句代码最终会被编译成多条汇编指令,它大致做了以下这三件事: (1)给Singleton的实例分配内存空间; (2)调用Singleton()的构造函数,初始化成员字段; (3)将instance对象指向分配的内存空间。(做完这一步后instance就不为null了) 但是,由于java编译器允许处理器乱序执行,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是1-2-3也可能是1-3-2。如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候instance因为在线程A内执行过了第三点,instance已经是非空了,所以,线程B直接取走instance,再使用时就会出错,这就是DCL失效问题。

在JDK1.5后,SUN官方已经注意到了这个问题,调整了JVM,并且具体化了volatile关键字,该关键字的作用是防止指令重排,就是说在实例化instance对象时,只能是1-2-3,不会出现执行顺序为1-3-2的情况。因此在JDK1.5版本或之后,DCL应该改写如下:

public class Singleton {
  //注意:相比上面的代码,这里加上了volatile关键字
  private volatile static Singleton instance; 
  private Singleton() {
  }

  public static Singleton getInstance() {
    if(instance == null) {
        synchronized(Singleton.class) {
          if(instance == null) {
              instance  = new Singleton();
          }
        }
    }
    return instance;
  }
}

当然,volatile或多或少也会影响性能,但考虑到程序的正确性,牺牲点性能还是值得的。

DCL的优点是:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。 DCL的缺点是:第一次加载时反应稍慢。 DCL模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性,除非你的代码在并发场景比较复杂或者低于JDK 6版本下使用,否则,这种方式一般能够满足需求。

三、静态内部类单例模式

public class Singleton {
  private Singleton() {}
  
  public static Singleton getInstance() {
    return InnerClass.instance;
  }
  //静态内部类
  private static class InnerClass {
    private static final Singleton instance = new Singleton();
  }
}

当第一次加载Singleton类时并不会初始化instance,只有在第一次调用Singleton的getInstance方法时才会导致instance被初始化。因此,第一次调用getInstance方法会导致虚拟机加载InnerClass类,这种方式不仅能确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方式。

四、枚举单例