单例模式
单例模式保证一个类在 JVM 中只有一个实例,并提供全局访问点。常见于配置对象、线程池、连接池、缓存管理器等场景。
为什么需要单例
有些对象不应该被反复创建:
- 创建成本高,例如连接池、线程池。
- 需要全局共享状态,例如配置中心客户端。
- 多个实例会造成资源冲突,例如本地缓存管理器。
mermaid
flowchart TD
A["多个调用方"] --> B["统一访问 getInstance"]
B --> C["同一个 Singleton 实例"]
C --> D["共享配置或资源"]如果本该全局唯一的对象被创建多个实例,可能出现:
| 场景 | 后果 |
|---|---|
| 多个线程池 | 线程数量失控 |
| 多个连接池 | 数据库连接被耗尽 |
| 多个本地缓存 | 数据不一致,内存浪费 |
| 多个配置管理器 | 配置刷新状态混乱 |
饿汉式
类加载时就创建实例。
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}优点:
- 写法简单。
- 类加载机制保证线程安全。
缺点:
- 类加载时就创建,不能懒加载。
- 如果对象很重,但可能不会被使用,会浪费资源。
懒汉式双重检查
对象第一次使用时创建。
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;
}
}创建流程:
mermaid
flowchart TD
A["调用 getInstance"] --> B{"instance 是否为空"}
B -- "否" --> C["直接返回"]
B -- "是" --> D["进入 synchronized"]
D --> E{"再次检查 instance"}
E -- "为空" --> F["创建对象"]
E -- "不为空" --> C
F --> C为什么要检查两次:
- 第一次检查:实例已经存在时不进入锁,提高性能。
- 第二次检查:多个线程排队进入锁后,防止重复创建。
为什么必须用 volatile:
text
instance = new Singleton()底层可以拆成:
- 分配内存。
- 调用构造方法初始化对象。
- 把引用赋值给
instance。
如果没有 volatile,步骤 2 和 3 可能发生重排序。另一个线程可能看到 instance != null,但对象还没初始化完成。
静态内部类
推荐写法之一,既懒加载,又线程安全。
java
public class Singleton {
private Singleton() {
}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}原理:外部类加载时不会立刻加载内部类,只有调用 getInstance() 时才加载 Holder,JVM 类加载机制保证初始化线程安全。
枚举单例
java
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("do something");
}
}枚举单例的优势:
- 写法简单。
- 天然防止反射破坏。
- 天然处理序列化问题。
如果不是需要继承类,枚举单例是很稳妥的写法。
Spring Bean 和单例模式
Spring Bean 默认是单例,但它是“Spring 容器级单例”,不是手写设计模式里的 JVM 全局单例。
| 对比 | 手写单例 | Spring 单例 Bean |
|---|---|---|
| 生命周期 | 自己控制 | Spring 容器控制 |
| 创建方式 | getInstance() | 依赖注入 |
| 测试替换 | 较困难 | 可以 Mock 或替换 Bean |
| 推荐程度 | 特殊场景使用 | Spring 项目更常用 |
在 Spring 项目里,业务 Service、Repository、Client 通常交给容器管理,不需要手写单例。
滥用风险
| 风险 | 后果 |
|---|---|
| 单例保存可变状态 | 多线程并发读写出错 |
| 全局访问到处调用 | 依赖关系隐藏,测试困难 |
| 构造复杂依赖 | 初始化顺序难控制 |
| 和 Spring 容器混用 | 生命周期混乱 |
错误示例:
java
public class UserContextHolder {
private static final UserContextHolder INSTANCE = new UserContextHolder();
private Long currentUserId;
}currentUserId 是请求级状态,不应该放在全局单例里,否则不同用户请求会互相污染。
小结
单例模式适合全局唯一、创建成本高、需要统一管理的对象。它的重点不是“少创建对象”,而是“控制共享资源的唯一入口”。在 Spring 项目中,优先使用容器管理单例;只有容器外工具类或特殊资源管理器才考虑手写单例。
