1. 什么是单例模式
单例模式(Singleton Pattern)是 GoF 23种设计模式中对象创建型设计模式之一,其核心思想是把构造函数藏起来,让外界拿不到 new 的入口,类内部提前(或第一次用时)new 好唯一实例,再通过一个全局访问点把这份实例交出去。
2.为什么需要单例模式
当系统中某些对象本质上全局唯一(如配置中心、线程池、缓存、日志对象、设备驱动、注册表、数据库连接池等)时,如果任由客户端随意 new,就会出现:
- 资源浪费:同一类实例被重复创建,占用多余内存与句柄;高并发场景下还可能把 JVM 或操作系统资源耗尽。
- 状态不一致:多个实例各自维护一份内部状态,导致配置、计数器、缓存、调度策略等数据出现“多份真相”,业务逻辑错乱。
- 竞争与冲突:对独占资源(串口、打印机等)的并发访问失去统一协调,易产生死锁、覆盖、重复初始化等问题。
- 难以热升级或监控:散落在各处的实例让“统一刷新配置”“统一收集指标”变得不可能,系统可运维性骤降。
单例模式通过“类只对外暴露一个全局访问点”并禁止外部 new,保证:
- 整个 JVM(或进程、容器)里仅存在一份实例,内存与资源开销可预测、可控制;
- 所有线程、所有模块看到的都是同一份状态,天然避免“副本漂移”;
- 初始化成本只发生一次(懒汉/饿汉/静态内部类/枚举等方式可灵活选择时机),后续调用近乎零开销;
- 客户端与具体类解耦——只依赖
getInstance()返回的接口,更换实现无需改动业务代码; - 可以方便地在此基础上做统一生命周期管理(刷新、销毁、监控、代理、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 容器统一管理。
一句话总结:单例模式就像你电脑里的任务管理器——无论点多少次,打开的都是同一个窗口。