Skip to content

单例模式(Singleton Pattern)

1. 官方定义

"Ensure a class only has one instance, and provide a global point of access to it."
—— 《Design Patterns: Elements of Reusable Object-Oriented Software》(GoF, 1994)

核心:确保一个类只有一个实例,并提供全局访问点。


2. 模式解释

核心思想

  • 唯一性控制:通过私有构造器禁止外部直接实例化对象。
  • 全局访问:通过静态方法(如 getInstance())提供唯一实例的访问入口。

优点

  • 资源优化:避免重复创建耗资源对象(如数据库连接池)。
  • 全局一致性:统一管理共享资源(如配置管理器、日志服务)。
  • 简化访问:无需传递对象引用,直接通过类方法获取实例。

缺点

  • 隐藏耦合:全局状态可能导致代码难以测试(如依赖单例的类无法隔离测试)。
  • 多线程风险:需额外处理线程安全问题(如双重检查锁)。
  • 违反单一职责原则:单例类同时承担自身业务和实例管理的职责。

3. 解决的问题

经典场景

  1. 全局配置管理
    • 应用中需要唯一配置中心(如 AppConfig 读取配置文件)。
  2. 共享资源访问
    • 线程池、缓存、日志记录器等需全局唯一实例。
  3. 硬件资源控制
    • 打印机后台服务、文件系统管理器等需独占访问的资源。

4. 实现注意事项

关键实现点

  1. 线程安全
    • 多线程环境下需防止重复创建实例(如使用双重检查锁)。
  2. 延迟加载(Lazy Initialization)
    • 仅在首次访问时创建实例,避免启动时资源浪费。
  3. 反序列化与反射攻击
    • 防止通过反射或反序列化破坏单例(如枚举实现天然防御)。
  4. 性能权衡
    • 同步锁可能引入性能开销(如饿汉式 vs 懒汉式)。

代码示例(Java)

java
// 双重检查锁实现(线程安全 + 延迟加载)
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {} // 私有构造器

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

5. 实现变体

四种常见实现方式

类型特点适用场景
饿汉式类加载时立即初始化实例(线程安全,但可能浪费资源)实例占用资源小,启动即需使用
懒汉式(锁)首次访问时初始化,需同步锁保证线程安全(性能较低)不推荐使用
双重检查锁延迟加载 + 线程安全(需 volatile 关键字防止指令重排)高并发场景
静态内部类利用类加载机制保证线程安全,延迟加载(推荐方式)通用场景
枚举单例天然防反射和反序列化攻击(《Effective Java》推荐方式)需严格防御破坏的场景

6. 相似模式对比

模式核心区别
工厂模式关注对象创建过程,而单例模式关注对象唯一性
享元模式管理多个可共享的相似对象,而单例模式管理唯一对象
全局变量单例模式通过封装控制实例化过程,避免全局变量的不可控性和命名污染

7. 组合使用场景

常见搭配模式

  1. 工厂模式
    • 场景:工厂类本身需要全局唯一(如数据库连接工厂)。
    • 组合方式:将工厂类实现为单例。
  2. 代理模式
    • 场景:为单例对象增加访问控制(如日志记录器的权限校验)。
  3. 抽象工厂模式
    • 场景:抽象工厂的具体实现类需保证唯一性(如跨平台 UI 工厂)。

8. 总结记忆点

  • 核心价值:确保全局唯一性,简化共享资源管理。
  • 线程安全优先级:枚举 > 静态内部类 > 双重检查锁 > 懒汉式锁。
  • 反模式警示:滥用单例会导致代码耦合度高、难以测试(优先考虑依赖注入)。
  • 适用性判断:当且仅当系统中确需严格唯一实例时使用(如硬件控制)。

附:单例模式的误用风险

  • 测试困难:单例的全局状态可能导致单元测试互相干扰(需通过依赖注入解耦)。
  • 内存泄漏:长时间持有的单例可能阻止资源回收(如 Android 中 Context 单例)。
  • 扩展性差:若未来需支持多实例,需重构大量代码(优先通过工厂模式预留扩展点)。