「设计模式」🌍单例模式(Singleton)

2,578 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

模式动机

对于系统中的某些类来说,只有一个实例很重要。例如:一个系统可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统 ...

如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。

一个更好的解决办法就是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法,这就是单例模式

单例模式同时解决了两个问题,所以违反了单一职责原则

  1. 保证一个类只有一个实例。为什么会有人想要控制一个类所拥有的实例数量?最常见的原因是控制某些共享资源(例如数据库或文件)的访问。
    ⭐注意:普通构造函数无法实现单一实例,因为构造函数的设计决定了它必须总是返回一个新对象。
  2. 为该实例提供一个全局访问节点。还记得你曾经用过的那些存储重要对象的全局变量吗?它们在使用上十分方便,但非常不安全,因为任何代码都可能覆盖掉其内容。和全局变量一样,单例模式也允许在程序的任何地方访问特定对象,但是它可以保护该实例不被其他代码覆盖。

如今,单例模式已非常流行,以至于人们时常将只解决如上描述的任意一个问题的东西都成为单例。

定义

单例模式属于创建型模式

单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法

UML 类图

模式结构

⭐所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数私有化 private,防止其他对象新建对象 new.
  • 新建一个静态构建方法作为构造函数。该函数会 “偷偷” 调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。

单例模式包含如下角色:

  • Singleton:单例类声明了一个名为 getInstance() 获取实例的静态方法来返回其所属类的一个相同实例。单例的构造函数必须对客户端 Client 因此,调用 getInstance() 必须是获取单例模式的唯一方式。

更多实例

在操作系统中,打印池是一个用于管理打印任务的应用程序,通过打印池用户可以删除、中止或者改变打印任务的优先级,在一个系统中只允许运行一个打印池对象,如果重复创建打印池则抛出异常。现使用单例模式来模拟实现打印池的设计。

示例代码

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

优缺点

✔提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户端如何以及何时访问,并为设计及开发团队提供了共享概念。

✔由于在系统内存中只存在一个对象,因此可以节省系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。

允许可变数目的实例。我们可以对单例模式进行改造,使用与单例模式相似的方法来获得指定个数的对象实例。

❌由于单例模式没有抽象层,所以对单例类进行扩展有很大的难度(此处指对类扩展而非模式)。

❌单例类的职责过重,既充当了工厂角色,又充当了产品角色;它同时解决了两个问题,违背了“单一职责原则”。

❌滥用单例模式会带来负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如 Java、C#)的运行环境都提供了自动垃圾回收的技术,所以如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

适用场景

在以下情况推荐使用单例模式:

(1)系统只需要一个实例。

(2)客户调用类的单个实例只允许使用一个公共访问点。

(3)在一个系统中要求一个类只有一个实例时才应当使用单例模式。反之,如果一个类可以有几个实例共存,就需要对单例模式进行改造,使之成为多例模式。

「单例模式」落地

(1)java.lang.Runtime

(2)一个具有自动编号主键的表可以有多个用户同时使用,但该主键编号生成器是同一个实例,才能沿用上一个已编号的主键 ID,否则会出现主键重复,所以可以通过单例模式实现。

(3)默认情况下,Spring 会通过单例模式创建 bean 实例:

<bean id="date" class="java.util.Date" scope="singleton"/>

模式扩展

以上所述的单例模式仅针对于单线程,接下来我们来考量引入多线程后可能发生的事情。

饿汉式单例模式

饿汉式单例类在自己被加载时就将自己实例化。单从资源利用效率角度来讲,这个比懒汉式单例类稍差些。从速度和反应时间角度来讲,则比懒汉式单例类稍好些。

public class EagerSingleton {
    private static EagerSingleton instance = new EagerSingleton();

    private EagerSingleton() {
    }

    public static EagerSingleton getInstance() {
        return instance;
    }
}

懒汉式单例模式

懒汉式单例类在第一次使用资源时才会实例化。有些资源初始化很可能耗费大量时间,延迟加载可以提高资源的利用效率。但懒汉式单例模式存在一个显著问题,必须处理多线程可能引发的问题,当多个线程同时首次引用此类静态方法时,很可能同时初始化多个实例,必须在首次实例化对象时对线程进行同步。

public class LazySingleton {
    private static LazySingleton instance = null;

    private LazySingleton() {
    }

    private static LazySingleton getInstance() {
        if (instance == null) {
            // 采用线程锁机制
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

懒汉式多例模式

使用单例模式的思想实现多例模式,确保系统中某个类的对象只能存在有限个。

public class LazyMultiInstance {
    private static LazyMultiInstance instance = null;
    // 用于记录当前实例化对象的个数
    private static volatile Integer count = 0;

    private LazyMultiInstance() {
    }

    public static LazyMultiInstance getInstance() {
        // 确保系统中该类的对象最多只有3个
        if (LazyMultiInstance.count < 3) {
            synchronized (LazyMultiInstance.class) {
                if (LazyMultiInstance.count < 3) {
                    instance = new LazyMultiInstance();
                }
            }
        }
        return instance;
    }
}

最后

👆上一篇:「设计模式」🌓原型模式(Prototype)

👇下一篇:「设计模式」🚢适配器模式(Adapter)

❤️ 好的代码无需解释,关注「手撕设计模式」专栏,跟我一起学习设计模式,你的代码也能像诗一样优雅!

❤️ / END / 如果本文对你有帮助,点个「赞」支持下吧,你的支持就是我最大的动力!