单例模式
单例模式
Rif设计模式笔记:单例模式 (Singleton Pattern)
一、一句话概括
确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
二、为什么需要它 (Why - The Pain Point)
在软件系统中,某些类我们希望它们在整个程序的生命周期中只存在一个实例。例如:
- 配置管理器:读取应用的配置信息,多份配置实例可能导致数据不一致。
- 日志记录器:所有日志都应写入同一个日志文件或流。
- 数据库连接池:管理数据库连接,通常只需要一个池来协调所有连接。
- 线程池:统一管理工作线程。
痛点:
如果允许客户端随意通过 new 关键字创建这些类的实例,会带来几个问题:
- 资源浪费:创建多个实例会占用不必要的内存和系统资源(比如多个数据库连接池会建立过多的物理连接)。
- 状态不一致:多个实例可能持有不同的状态,导致应用行为混乱、不可预测。
- 缺乏统一入口:客户端需要自己管理这些对象的生命周期,无法保证全局使用的都是同一个实例。
核心问题:如何从机制上强制一个类只能被实例化一次,并为整个系统提供一个方便、统一的访问方式?
三、它是什么 (What - The Solution)
单例模式通过控制类的创建过程来解决这个问题。它将类的构造函数私有化,并提供一个静态方法作为获取唯一实例的入口。
核心思想和实现方式:
单例模式有多种实现方式,各有优劣,尤其是在多线程环境下需要特别注意。
饿汉式 (Eager Initialization)
- 思想:在类加载时就立即创建实例,不管之后用不用。
- 优点:实现简单,天生线程安全(因为JVM在类加载阶段保证了线程安全)。
- 缺点:如果实例创建很耗时或占用资源大,但程序一直没用它,会造成资源浪费。
懒汉式 (Lazy Initialization)
- 思想:等到第一次被请求时才创建实例。
- 优点:实现了懒加载,避免了资源浪费。
- 缺点:在多线程环境下,如果不加同步措施,可能会创建出多个实例。
双重检查锁定 (Double-Checked Locking, DCL) - 懒汉式的优化
- 思想:在懒汉式的基础上,通过两次
if (instance == null)检查和synchronized块来减少同步开销,提高性能。 - 注意:需要使用
volatile关键字修饰实例变量,以防止指令重排序问题。这是面试中的高频考点。
- 思想:在懒汉式的基础上,通过两次
静态内部类 (Static Inner Class)
- 思想:利用JVM类加载机制来保证线程安全和懒加载。实例在静态内部类中创建,只有当
getInstance()方法第一次被调用时,才会触发内部类的加载。 - 优点:兼具线程安全和懒加载,实现优雅,是目前最推荐的实现方式之一。
- 思想:利用JVM类加载机制来保证线程安全和懒加载。实例在静态内部类中创建,只有当
枚举 (Enum)
- 思想:利用枚举类型的特性,一个枚举元素就是该类的一个天然单例。
- 优点:代码最简洁,由JVM保证绝对的线程安全,并且能天然防止通过反射和反序列化来破坏单例。被《Effective Java》作者 Joshua Bloch 极力推荐。
四、怎么做 (How - The Blueprint)
基本步骤(以最推荐的静态内部类方式为例):
- 将类的构造函数声明为
private,防止外部直接new。 - 创建一个
private static的静态内部类。 - 在静态内部类中,创建一个
public static final的外部类实例。 - 提供一个
public static的getInstance()方法,返回静态内部类中的实例。
Java 示例代码 (静态内部类实现)
1 | // 推荐的单例实现方式 |
五、UML 类图
1 | @startuml |
(注意: UML图表示的是基本概念,无法完全体现DCL、静态内部类等复杂实现的细节)
六、优缺点分析
优点 (Pros):
- 保证唯一实例:从根本上控制了实例数量,节约了资源。
- 全局访问点:提供了一个方便的全局访问入口。
- 懒加载(部分实现):懒汉式、DCL、静态内部类等实现方式可以延迟实例的创建。
缺点 (Cons):
- 违反单一职责原则:一个类既要负责自身的业务逻辑,又要负责保证自己是单例,职责不够单一。
- 对测试不友好:单例模式的全局状态使得单元测试变得困难,因为测试用例之间会相互影响。很难用 mock 对象来替换单例。
- 扩展性差:单例类通常是
final的或者构造函数私有,难以继承。 - 生命周期管理:在某些语言(如C++)中,单例的销毁时机是一个难题。在Java中,虽然有GC,但也需要注意内存泄漏问题。