23种设计模式之单例模式

121 阅读5分钟

1. 什么是单例模式

单例模式(Singleton Pattern)是 GoF 23种设计模式中对象创建型设计模式之一,其核心思想是把构造函数藏起来,让外界拿不到 new 的入口,类内部提前(或第一次用时)new 好唯一实例,再通过一个全局访问点把这份实例交出去。

2.为什么需要单例模式

当系统中某些对象本质上全局唯一(如配置中心、线程池、缓存、日志对象、设备驱动、注册表、数据库连接池等)时,如果任由客户端随意 new,就会出现:

  • 资源浪费:同一类实例被重复创建,占用多余内存与句柄;高并发场景下还可能把 JVM 或操作系统资源耗尽。
  • 状态不一致:多个实例各自维护一份内部状态,导致配置、计数器、缓存、调度策略等数据出现“多份真相”,业务逻辑错乱。
  • 竞争与冲突:对独占资源(串口、打印机等)的并发访问失去统一协调,易产生死锁、覆盖、重复初始化等问题。
  • 难以热升级或监控:散落在各处的实例让“统一刷新配置”“统一收集指标”变得不可能,系统可运维性骤降。

单例模式通过“类只对外暴露一个全局访问点”并禁止外部 new,保证:

  1. 整个 JVM(或进程、容器)里仅存在一份实例,内存与资源开销可预测、可控制;
  2. 所有线程、所有模块看到的都是同一份状态,天然避免“副本漂移”;
  3. 初始化成本只发生一次(懒汉/饿汉/静态内部类/枚举等方式可灵活选择时机),后续调用近乎零开销;
  4. 客户端与具体类解耦——只依赖 getInstance() 返回的接口,更换实现无需改动业务代码;
  5. 可以方便地在此基础上做统一生命周期管理(刷新、销毁、监控、代理、AOP 等),提升可维护性。

因此,单例模式特别适用于“系统中必须且只能有唯一一个对象”的场景,是节约资源、保证一致性、简化协调的基石型手段。

3. Java代码示例

第一种实现方式(标准实现):双重检查锁定 + volatile

这是最常用且线程安全的单例模式实现方式,配合 volatile 关键字防止指令重排序。这是兼顾性能与线程安全的标准实现。

public class Singleton {
    // 使用 volatile 禁止指令重排序,确保多线程环境下正确初始化
    private static volatile Singleton instance;

    // 私有构造函数,防止外部实例化
    private Singleton() {
        // 防止通过反射攻击破坏单例(可选增强)
        if (instance != null) {
            throw new RuntimeException("请使用 getInstance() 获取单例实例");
        }
    }

    // 提供全局访问点
    public static Singleton getInstance() {
        // 第一次检查(无锁,提高性能)
        if (instance == null) { 
            synchronized (Singleton.class) {
                // 第二次检查(确保只有一个线程创建实例)
                if (instance == null) { 
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

第二种实现方式:静态内部类

利用类加载机制保证线程安全,延迟加载,代码简洁

public class Singleton {
    private Singleton() {}

    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

第三种实现方式:枚举

《Effective Java》作者 Joshua Bloch 推荐此方式,天然防止反射、序列化等问题。

public enum Singleton {
    INSTANCE;

    // 可添加方法
    public void doSomething() {
        // ...
    }
}

4. 优缺点

维度优点(Pros)缺点(Cons)
资源控制确保系统中仅存在一个实例,有效节省内存和系统资源(如数据库连接池、配置管理器等)。容易被滥用,将本应由容器或依赖注入管理的对象硬编码为单例,掩盖了设计耦合问题。
全局访问提供统一、便捷的全局访问点,简化对象获取逻辑。引入隐式全局状态,导致模块间紧耦合,违反“显式依赖”原则,降低代码可读性和可维护性。
线程安全通过双重检查锁定(DCL + volatile)、静态内部类或枚举等方式可实现线程安全。实现不当(如未加锁的懒汉式)会导致多线程下创建多个实例;加锁虽安全但可能影响高并发性能(现代 JVM 优化后影响较小)。
延迟初始化懒加载实现(如 DCL、静态内部类)支持按需创建实例,避免应用启动时不必要的开销。饿汉式在类加载时即创建实例,即使未使用也会占用资源,无法实现延迟加载。
安全性枚举实现天然防止通过反射或反序列化创建新实例,是最安全的单例形式。普通实现若未重写 readResolve() 方法,反序列化会生成新对象;反射可绕过私有构造函数(需额外校验防护)。
性能避免重复创建对象,减少内存分配和垃圾回收压力。懒加载首次调用时需同步(如加锁),有轻微性能开销;但后续访问无锁,整体性能良好。

5. 典型应用

  • Java Runtime.getRuntime()Runtime 类代表 Java 虚拟机(JVM)的运行时环境。每个 JVM 进程只能有一个运行时实例,用于执行如启动外部进程、获取内存信息等操作。
  • Spring Bean(默认单例):在 Spring 框架中,Bean 的作用域(scope)默认是 singleton,即在整个 Spring IoC 容器中,某个 Bean 类型只会有一个实例。Spring 并不是通过传统单例模式(私有构造函数 + getInstance) 实现的,而是由 IoC 容器统一管理。

一句话总结:单例模式就像你电脑里的任务管理器——无论点多少次,打开的都是同一个窗口。