单例模式

设计模式笔记:单例模式 (Singleton Pattern)

一、一句话概括

确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。

二、为什么需要它 (Why - The Pain Point)

在软件系统中,某些类我们希望它们在整个程序的生命周期中只存在一个实例。例如:

  • 配置管理器:读取应用的配置信息,多份配置实例可能导致数据不一致。
  • 日志记录器:所有日志都应写入同一个日志文件或流。
  • 数据库连接池:管理数据库连接,通常只需要一个池来协调所有连接。
  • 线程池:统一管理工作线程。

痛点:
如果允许客户端随意通过 new 关键字创建这些类的实例,会带来几个问题:

  1. 资源浪费:创建多个实例会占用不必要的内存和系统资源(比如多个数据库连接池会建立过多的物理连接)。
  2. 状态不一致:多个实例可能持有不同的状态,导致应用行为混乱、不可预测。
  3. 缺乏统一入口:客户端需要自己管理这些对象的生命周期,无法保证全局使用的都是同一个实例。

核心问题:如何从机制上强制一个类只能被实例化一次,并为整个系统提供一个方便、统一的访问方式?

三、它是什么 (What - The Solution)

单例模式通过控制类的创建过程来解决这个问题。它将类的构造函数私有化,并提供一个静态方法作为获取唯一实例的入口。

核心思想和实现方式:

单例模式有多种实现方式,各有优劣,尤其是在多线程环境下需要特别注意。

  1. 饿汉式 (Eager Initialization)

    • 思想:在类加载时就立即创建实例,不管之后用不用。
    • 优点:实现简单,天生线程安全(因为JVM在类加载阶段保证了线程安全)。
    • 缺点:如果实例创建很耗时或占用资源大,但程序一直没用它,会造成资源浪费。
  2. 懒汉式 (Lazy Initialization)

    • 思想:等到第一次被请求时才创建实例。
    • 优点:实现了懒加载,避免了资源浪费。
    • 缺点:在多线程环境下,如果不加同步措施,可能会创建出多个实例。
  3. 双重检查锁定 (Double-Checked Locking, DCL) - 懒汉式的优化

    • 思想:在懒汉式的基础上,通过两次 if (instance == null) 检查和 synchronized 块来减少同步开销,提高性能。
    • 注意:需要使用 volatile 关键字修饰实例变量,以防止指令重排序问题。这是面试中的高频考点。
  4. 静态内部类 (Static Inner Class)

    • 思想:利用JVM类加载机制来保证线程安全和懒加载。实例在静态内部类中创建,只有当 getInstance() 方法第一次被调用时,才会触发内部类的加载。
    • 优点:兼具线程安全和懒加载,实现优雅,是目前最推荐的实现方式之一。
  5. 枚举 (Enum)

    • 思想:利用枚举类型的特性,一个枚举元素就是该类的一个天然单例。
    • 优点:代码最简洁,由JVM保证绝对的线程安全,并且能天然防止通过反射和反序列化来破坏单例。被《Effective Java》作者 Joshua Bloch 极力推荐。

四、怎么做 (How - The Blueprint)

基本步骤(以最推荐的静态内部类方式为例):

  1. 将类的构造函数声明为 private,防止外部直接 new
  2. 创建一个 private static静态内部类
  3. 在静态内部类中,创建一个 public static final 的外部类实例。
  4. 提供一个 public staticgetInstance() 方法,返回静态内部类中的实例。

Java 示例代码 (静态内部类实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 推荐的单例实现方式
public class Singleton {

// 1. 私有化构造函数
private Singleton() {
// 防止通过反射创建实例
if (SingletonHolder.INSTANCE != null) {
throw new IllegalStateException("Singleton instance already created. Use getInstance() method.");
}
}

// 2. 创建一个私有的静态内部类
private static class SingletonHolder {
// 3. 在内部类中创建外部类的唯一实例
// JVM保证了类加载的线程安全
private static final Singleton INSTANCE = new Singleton();
}

// 4. 提供全局访问点
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

// 示例方法
public void showMessage() {
System.out.println("Hello from the Singleton instance! HashCode: " + this.hashCode());
}
}

// 客户端
public class SingletonPatternDemo {
public static void main(String[] args) {
// 无法通过 new 创建: new Singleton(); // 编译错误

// 通过 getInstance() 获取唯一实例
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();

// 验证是否是同一个实例
System.out.println("instance1 == instance2 ? " + (instance1 == instance2));

instance1.showMessage();
instance2.showMessage();
}
}

五、UML 类图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@startuml
skinparam classAttributeIconSize 0
hide empty members

class Singleton {
- {static} Singleton instance
- Singleton()
+ {static} Singleton getInstance()
+ void showMessage()
}

class SingletonPatternDemo {
}

SingletonPatternDemo ..> Singleton : uses

note on link: getInstance()

@enduml

(注意: UML图表示的是基本概念,无法完全体现DCL、静态内部类等复杂实现的细节)

六、优缺点分析

优点 (Pros):

  1. 保证唯一实例:从根本上控制了实例数量,节约了资源。
  2. 全局访问点:提供了一个方便的全局访问入口。
  3. 懒加载(部分实现):懒汉式、DCL、静态内部类等实现方式可以延迟实例的创建。

缺点 (Cons):

  1. 违反单一职责原则:一个类既要负责自身的业务逻辑,又要负责保证自己是单例,职责不够单一。
  2. 对测试不友好:单例模式的全局状态使得单元测试变得困难,因为测试用例之间会相互影响。很难用 mock 对象来替换单例。
  3. 扩展性差:单例类通常是 final 的或者构造函数私有,难以继承。
  4. 生命周期管理:在某些语言(如C++)中,单例的销毁时机是一个难题。在Java中,虽然有GC,但也需要注意内存泄漏问题。