12 08 2020

前言

在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。

单例模式的定义

单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。

例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。

在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。

在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型。

J2EE 标准中的 ServletContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。

单例模式的特点

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点。

单例模式的优点和缺点

单例模式的优点:

  1. 单例模式可以保证内存里只有一个实例,减少内存开销。
  2. 可以避免对资源的多重占用。
  3. 单例模式设置全局访问点,可以优化和共享资源的访问。

单例模式的缺点:

  1. 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  2. 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  3. 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

单例模式的应用场景

对于 Java 来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面:

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
  • 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁访问数据库或文件的对象。
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

单例模式的结构与实现

单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

下面来分析其基本结构和实现方法。

单例模式的结构

单例模式的主要角色如下:

  • 单例类:包含一个实例且能自行创建这个实例的类。
  • 访问类:使用单例的类。

其结构如图所示:

单例模式的实现

为了更好的对下面即将介绍的几种实现方式进行比较,我们定义几个考量维度:线程的安全线、是否懒加载、性能。

1. 饿汉模式(线程安全)
  1. //不允许被继承
  2. public final class Singleton {
  3. //定义实例变量的时候直接初始化
  4. //instance 被 ClassLoader 加载后很长一段时间才被使用,它所开辟的堆内存会驻留更久
  5. private static Singleton instance = new Singleton();
  6. //私有构造函数,不允许外部 new
  7. private Singleton() {
  8. }
  9. //无法进行懒加载
  10. public static Singleton getInstance() {
  11. return instance;
  12. }
  13. }
  • 优点:在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快,这种方式基于类加载机制避免了多线程的同步问题(线程安全)。
  • 缺点:在于实例在类初始化的时候就创建了,如果在整个项目中都没有使用到该类,就会创建内存空间的浪费。
2. 懒汉模式(线程不安全)
  1. //不允许被继承
  2. public final class Singleton {
  3. //定义实例,不初始化
  4. private static Singleton instance = null;
  5. //私有构造函数,不允许外部new
  6. private Singleton() {
  7. }
  8. //可保证实例的懒加载,但无法保证实例的唯一性
  9. public static Singleton getInstance() {
  10. if (instance == null) {
  11. instance = new Singleton();
  12. }
  13. return instance;
  14. }
  15. }
  • 优点:懒汉模式申明了一个静态对象,在用户第一次调用时初始化,节约了资源,保证实例的懒加载。
  • 缺点:多线程下不安全,无法保证实例的唯一性。
3. 懒汉模式+同步方法(线程安全)
  1. public final class Singleton {
  2. //定义实例,不初始化
  3. private static Singleton instance = null;
  4. //私有构造函数,不允许外部new
  5. private Singleton() {
  6. }
  7. //向getInstance()加入同步控制,每次只能有一个线程进入
  8. public static synchronized Singleton getInstance() {
  9. if (instance == null) {
  10. instance = new Singleton();
  11. }
  12. return instance;
  13. }
  14. }
  • 优点:在多线程下保证了数据的同步,这种方式满足了即懒加载又能保证 instance 实例的唯一性。
  • 缺点:由于使用了 synchronized 的排他性导致 getInstance() 同一时刻只能被一个线程访问,性能低下。
4. 双重检查模式+volatile(线程安全)
  1. public final class Singleton {
  2. //定义实例,不初始化,volatile 防止指令重排,
  3. private static volatile Singleton instance = null;
  4. Connection conn;
  5. Socket socket;
  6. //私有构造函数,不允许外部new
  7. private Singleton() {
  8. //初始化conn
  9. this.conn;
  10. //初始化socket
  11. this.socket;
  12. }
  13. public static Singleton getInstance() {
  14. //当instance为null时,进入同步代码块,同时可避免了每次都需要进入同步代码块,可提高效率
  15. if (instance == null) {
  16. synchronized (Singleton.class) {
  17. //判断如果instance为null时创建
  18. if (instance == null) {
  19. instance = new Singleton();
  20. }
  21. }
  22. }
  23. return instance;
  24. }
  25. }

如果不使用 volatile,在实例化 conn、socket、Singleton 时,根据 JVM 运行时指令重排和 Happens-Before 规则,3者并无前后关系约束。可能 Singleton 先实例化,而 conn 和 socket 未完成实例化,未完成初始化的实例调用其方法将会抛出空指针异常。

  • 优点:这种方式满足多线程下的单例、懒加载以及获取实例的高效性。
5. 静态内部类模式(Holder方式)(线程安全)【推荐】
  1. public final class Singleton {
  2. //私有构造函数,不允许外部new
  3. private Singleton() {
  4. }
  5. //在静态内部类中持有Singleton的实例,并且可直接被初始化
  6. private static class SingletonHolder {
  7. private static Singleton instance = new Singleton();
  8. }
  9. //调用getInstance方法,事实上获得Holder的instance静态属性
  10. public static Singleton getInstance() {
  11. return SingletonHolder.instance;
  12. }
  13. }

在Singleton初始化的过程中不会创建Singleton的实例,静态内部类Holder中定义了Singleton的静态变量,并直接实例化,当Holder被主动引用时会创建Singleton的实例,Singleton实例的创建在Java编译时期收集在()中,该方法又是同步方法,可以保证内存的可见性、JVM指令的顺序性以及原子性。

6. 枚举方式【推荐】
  1. public enum Singleton {
  2. INSTANCE;
  3. Singleton() {
  4. System.out.println("INSTANCE will be initialized immediately");
  5. }
  6. private static void doSomething(){
  7. //调用该方法会主动使用Singleton,INSTANCE将会被实例化
  8. }
  9. //调用getInstance方法,事实上获得Holder的instance静态属性
  10. public static Singleton getInstance(){
  11. return INSTANCE;
  12. }
  13. }

枚举类型不允许被继承,同时是线程安全且只能被实例化一次,但是枚举类型不能够懒加载,对Singleton主动使用,如用其中的静态方法 INSTANCE 会立即实例化。

可对其进行改造,增加懒加载的特性,类似于Holder方式:

  1. public class Singleton {
  2. private Singleton() {
  3. System.out.println("init");
  4. }
  5. private enum EnumHolder {
  6. INSTANCE;
  7. private Singleton instance;
  8. EnumHolder() {
  9. this.instance = new Singleton();
  10. }
  11. public Singleton getSingleton() {
  12. return instance;
  13. }
  14. }
  15. public static Singleton getInstance() {
  16. return EnumHolder.INSTANCE.getSingleton();
  17. }
  18. }
延伸阅读
  1. 抽象工厂模式
  2. 工厂方法模式
  3. 单例模式
  4. 面向对象设计原则-迪米特法则
  5. 面向对象设计原则-合成复用原则
发表评论