单例 Singleton

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。


为什么要使用?

单例模式的对象职责:

  • 保证一个类只有一个实例。有时候我们需要控制一个类所拥有的实例数量,例如控制某些共享资源(数据库或文件)的访问权限。但是如果使用构造函数创建对象,它总是返回一个新的对象,不符合我们的需求。
  • 为该实例提供一个全局访问节点。我们常常会使用一些全局变量来让任何地方能够访问到,它很方便但是也会带来一些麻烦,例如会被其他代码覆盖掉那些变量的内容。但单例模式不会,它可以保护实例不被其他代码覆盖,同时它类似于全局变量或全局函数的角色可以在程序的任何地方被访问。

模式结构

  • 一个单例类只能有一个实例;(对象实例数量受到限制的事实)
  • 单例类必须自行创建这个实例;(对象实例的构造与销毁)
  • 单例类必须保证全局其他对象都能唯一访问到它。(需要保证对象实例成为“线程安全”的某种机制)

单例模式的类图:

通过一个私有的类变量来保存实例,再使用私有构造方法来确保外部无法通过 new 的方式创建新的实例。最后再为这个私有变量实例提供一个外部都能访问的方法。这样就保证了一个类只有一个实例, 并且全局都能访问到它。


模式实现

该示例使用饿汉式单例模式实现,通过判断两个实例是否相同来验证是否为单例模式。

示例程序的类图

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package example.simple;

/** 简单单例模式实现(饿汉式) */
public class Singleton {
/** 唯一实例 */
private static Singleton instance = new Singleton();

/** 禁止外部使用构造函数创建对象 */
private Singleton() {
System.out.println("Singleton类内部生成了一个实例!");
}

/** 提供外部可访问唯一实例的节点 */
public static Singleton getInstance() {
return instance;
}
}

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import example.simple.Singleton;

/** 测试 */
public class Test {
public static void main(String[] args) {
// 第一次获取实例
Singleton s1 = Singleton.getInstance();
// 第二次获取实例
Singleton s2 = Singleton.getInstance();
if (s1 == s2) {
System.out.println("s1和s2是同一个实例!");
} else {
System.out.println("s1和s2不是同一个实例!");
}
}
}

输出结果

1
2
Singleton类内部生成了一个实例!
s1和s2是同一个实例!

输出结果说明对象只被创建了一次,同时外部多次访问的实例都是同一实例。


常用场景和解决方案

常见的单例模式应用和使用的解决方案:

  • 饿汉式初始化:初始化的时候就创建对象。饿汉式创建对象的性能比较低但不存在线程同步问题,开发常用。
  • 懒汉式初始化:对象被调用时才创建对象。在单线程环境没有问题,但在多线程环境下就会出现问题,存在创建多个对象的风险。
  • 同步信号:懒汉单例模式在单线程环境没有问题,但在多线程环境下就会出现问题,存在创建多个对象的风险。所以就需要加锁同步。加锁也有两种方案:
    1. 锁方法:由于初始化实例就执行一次,所以锁方法会导致效率低下。
    2. 锁实例化代码块:在判空之后加入锁,锁住实例化代码块。但是这样也会造成同步问题,如果有多个线程进入了判空之后拿锁的阶段,此时第一个拿到锁的线程实例化后,第二个拿到锁的又能再实例化,不符合单例模式的设计。我们可以使用双重锁定来解决。
  • 双重锁定:双重锁定是一种较为实用、安全的方案,就是在同步产生实例化的代码块里再判断,进行两次检查。在双重检查锁中,代码会检查两次单例类是否有已存在的实例,一次加锁一次不加锁,一次确保不会有多个实例被创建。
  • 使用 ThreadLocal最常用的方式。ThreadLocal 会为每个线程提供一个独立的对象副本,从而解决多个线程对数据的访问冲突问题。

模式的优缺点

优点 缺点
你可以保证一个类只有一个实例。 违反了单一职责原则。 该模式同时解决了两个问题。
你获得了一个指向该实例的全局访问节点。 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
仅在首次请求单例对象时对其进行初始化。 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。

使用单例模式的优势

  • 对有限资源的合理利用,保护有限的资源,防止资源重复竞抢。
  • 更高内聚的代码组件,能提高代码复用性。
  • 具备全局唯一访问点的权限控制,方便按照统一规则管控权限。
  • 从负载均衡的角度考虑,可以轻松将 Singleton 扩展成两个、三个或更多个实例。由于封装了基数问题,所以在适当的时候可以自由更改实例的数量。

使用单例模式的劣势

  • 作为全局变量使用时,引用的对象越多,代码修改影响的范围就越大。
  • 作为全局变量时,在全局变量中使用状态变量时,会造成加/解锁的性能损耗。
  • 即便能扩展多实例,但耦合度依然很高,因为隐蔽了不同对象之间的调用关系。
  • 不支持有参数的构造函数。

拓展知识

在多数情况下,以下模式只会生成一个实例。

  • 抽象工程 Abstract Factory
  • 生成器 Builder
  • 外观 Facade
  • 原型 Prototype


🔙 设计模式

📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!

🔗参考文献:

🌐 设计模式 –refactoringguru

▶️ bilibili-趣学设计模式;黄靖锋. –拉勾教育

📖 图解设计模式 /(日)结城浩著;杨文轩译. –北京:人民邮电出版社,2017.1