走进 Gang of Four 设计模式:单例模式
走进 Gang of Four 设计模式:单例模式
说明:不仅告诉你"怎么用",更告诉你"为什么这样设计"
前置知识:Java 面向对象基础(构造方法、static、synchronized)、多线程基础
📊 设计模式分类总览
GoF 23 种设计模式可按两个维度交叉分类:类/对象(处理方式) × 创建型/结构型/行为型(目的)。
| 维度 | 创建型(Creational) | 结构型(Structural) | 行为型(Behavioral) |
|---|---|---|---|
| 类(Class) (通过继承复用) | Factory Method 工厂方法 | Adapter(类适配器) | Interpreter(解释器) Template Method(模板方法) |
| 对象(Object) (通过组合/聚合复用) | Abstract Factory(抽象工厂) Builder(建造者) Prototype(原型) Singleton(单例) | Adapter(对象适配器) Bridge(桥接) Composite(组合) Decorator(装饰) Facade(外观) Flyweight(享元) Proxy(代理) | Chain of Resp.(责任链) Command(命令) Iterator(迭代器) Mediator(中介者) Memento(备忘录) Observer(观察者) State(状态) Strategy(策略) Visitor(访问者) |
本文档聚焦 创建型-对象模式 中的 Singleton(单例),其余创建型对象模式包括 Abstract Factory、Builder、Prototype。
📑 目录
1. 模式概述
1.1 要解决什么问题?
核心矛盾:某些对象在系统中只需要一个实例(如配置管理器、连接池、日志工厂),但如果每次使用时都 new 一个,会导致资源浪费、状态不一致甚至系统错误。
// ❌ 反面教材:每次 new 一个,资源浪费且状态无法统一
ConfigManager cfg1 = new ConfigManager();
ConfigManager cfg2 = new ConfigManager();
cfg1.set("theme", "dark");
System.out.println(cfg2.get("theme")); // null —— 状态不同步!
解决方案:确保一个类只有一个实例,并提供全局访问点。
1.2 模式定义
单例模式确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
这是 GoF 《设计模式》中定义的创建型模式,也是 Java 开发中最常用、也最容易用错的模式之一。
1.3 三个核心特征
| 特征 | 说明 |
|---|---|
| 私有构造方法 | 防止外部通过 new 创建实例 |
| 自我实例化 | 类自己负责创建唯一实例(静态成员变量) |
| 全局访问点 | 提供静态方法 getInstance() 返回该实例 |
2. 六种实现方式
2.1 懒汉式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
| 维度 | 评价 |
|---|---|
| Lazy 初始化 | ✅ 是 |
| 线程安全 | ❌ 否(多线程下可能创建多个实例) |
| 实现难度 | ⭐ 易 |
风险:线程 A 执行到 if (instance == null) 后切换,线程 B 也执行到同一位置,两个线程会各自创建实例,破坏了单例。
2.2 懒汉式(线程安全)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
| 维度 | 评价 |
|---|---|
| Lazy 初始化 | ✅ 是 |
| 线程安全 | ✅ 是(但代价大) |
| 实现难度 | ⭐ 易 |
问题:synchronized 加在整个方法上,每次调用 getInstance() 都需要获取锁,即使实例已经创建。并发高时性能损失严重。
2.3 饿汉式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
| 维度 | 评价 |
|---|---|
| Lazy 初始化 | ❌ 否(类加载时即创建) |
| 线程安全 | ✅ 是(类加载线程安全由 JVM 保证) |
| 实现难度 | ⭐ 易 |
优点:实现最简单,无锁、线程安全。
代价:类加载时就创建实例,如果该实例从未被使用,会造成内存浪费。
2.4 双检锁(DCL, Double-Checked Locking)
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getSingleton() {
if (singleton == null) { // 第一次检查
synchronized (Singleton.class) { // 加锁
if (singleton == null) { // 第二次检查
singleton = new Singleton();
}
}
}
return singleton;
}
}
| 维度 | 评价 |
|---|---|
| Lazy 初始化 | ✅ 是 |
| 线程安全 | ✅ 是 |
| 实现难度 | ⭐⭐⭐ 较复杂 |
关键细节:
| 要素 | 作用 |
|---|---|
第一次检查 if (singleton == null) | 避免实例已创建后不必要的加锁,提升性能 |
synchronized | 保证同一时间只有一个线程进入创建代码块 |
第二次检查 if (singleton == null) | 防止多个线程同时通过第一次检查后重复创建 |
volatile | 禁止指令重排序,确保 new Singleton() 的写操作对其他线程立即可见 |
为什么需要
volatile?singleton = new Singleton()在 JVM 中分为三步:① 分配内存 ② 初始化对象 ③ 将引用指向内存。如果发生指令重排序变成 ①→③→②,线程 B 在步骤③后看到引用不为 null,直接返回了未初始化完成的对象,导致使用时报错。volatile禁止了这种重排序。
2.5 静态内部类(Holder 模式)
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
| 维度 | 评价 |
|---|---|
| Lazy 初始化 | ✅ 是(内部类在被调用时才加载) |
| 线程安全 | ✅ 是(类加载机制保证) |
| 实现难度 | ⭐⭐ 中 |
原理:JVM 在类加载阶段会执行 <clinit>() 方法,该方法由 JVM 自身保证同步。SingletonHolder 类只有在 getInstance() 方法中首次被引用时才会被 JVM 加载,此时才创建 INSTANCE,实现了延迟加载 + 线程安全 + 零同步开销的完美组合。
这是兼顾性能与简洁性的最佳实践,在不需要处理序列化/反射攻击的常规场景下优先推荐。
2.6 枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {
// 业务方法
}
}
| 维度 | 评价 |
|---|---|
| Lazy 初始化 | ❌ 否 |
| 线程安全 | ✅ 是 |
| 实现难度 | ⭐ 易 |
优势(《Effective Java》作者 Josh Bloch 提倡):
| 防护维度 | 说明 |
|---|---|
| 序列化安全 | 枚举的序列化由 JVM 保证单例,不会像普通类那样反序列化时创建新实例 |
| 反射安全 | 反射不能通过 Constructor.newInstance() 创建枚举实例 |
| 线程安全 | 枚举实例的创建由 JVM 保证 |
| 代码简洁 | 一行代码完成单例定义 |
如果需要绝对防止反射攻击和序列化破坏,枚举是实现单例模式的最佳方案。
3. 六种实现对比总表
| 实现方式 | Lazy | 线程安全 | 锁开销 | 反射安全 | 序列化安全 | 推荐指数 |
|---|---|---|---|---|---|---|
| 懒汉式(非线程安全) | ✅ | ❌ | 无 | ❌ | ❌ | ⭐ |
| 懒汉式(线程安全) | ✅ | ✅ | 每次调用 | ❌ | ❌ | ⭐⭐ |
| 饿汉式 | ❌ | ✅ | 无 | ❌ | ❌ | ⭐⭐⭐⭐ |
| 双检锁(DCL) | ✅ | ✅ | 首次创建 | ❌ | ❌ | ⭐⭐⭐ |
| 静态内部类 | ✅ | ✅ | 无 | ❌ | ❌ | ⭐⭐⭐⭐⭐ |
| 枚举 | ❌ | ✅ | 无 | ✅ | ✅ | ⭐⭐⭐⭐⭐ |
选型建议:
- 常规项目优先选 静态内部类(简洁、线程安全、支持延迟加载)
- 需要序列化/反射防护用 枚举
- 无延迟加载需求且实现越简单越好用 饿汉式
- JDK 自身源码如
Runtime就用了饿汉式
4. Spring 单例分析
4.1 核心区别:作用域不同
这是经典单例与 Spring 单例最本质的区别:
| 对比维度 | 经典单例模式 | Spring Singleton Bean |
|---|---|---|
| 控制粒度 | 限制在 ClassLoader 级别(一个 JVM 一个实例) | 限制在 Spring IoC 容器 级别(一个容器内一个 Bean ID 一个实例) |
| 实现方式 | 私有构造方法 + 静态方法(如 DCL、静态内部类) | 属性配置/注解(@Scope("singleton"))+ 单例注册表 |
| 实例化时机 | 饿汉:类加载时;懒汉:首次调用 getInstance() | 默认容器启动时初始化(可 @Lazy 改为懒加载) |
| 可测试性 | 差,构造方法私有,难以 Mock 和继承 | 好,普通 POJO,构造方法公开,方便单元测试 |
| 灵活性 | 一个类在 JVM 中只能有一个实例 | 同一个类可注册为多个不同名的 Bean(如多数据源) |
4.2 Spring 的实现机制:单例注册表
Spring 没有使用私有构造方法来限制实例创建,而是使用单例注册表(Singleton Registry)—— 一个 ConcurrentHashMap:
// Spring 内部核心逻辑简化示意
public class DefaultSingletonBeanRegistry {
// 一级缓存:存放完整的、可用的单例 Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = singletonFactory.getObject(); // 通过反射创建
this.singletonObjects.put(beanName, singletonObject);
}
return singletonObject;
}
}
}
流程:请求 Bean → 查缓存 → 有则直接返回 → 无则通过反射创建 → 放入缓存 → 返回。
这意味着 Spring 管理的 Bean 其构造方法通常是
public的,单例的约束来自 IoC 容器的缓存管理,而不是 Java 语言层面的访问控制。
4.3 三级缓存与循环依赖
Spring 在 DefaultSingletonBeanRegistry 中定义了三级缓存,用于解决单例 Bean 之间的循环依赖问题(如 A 依赖 B,B 依赖 A)。
三级缓存结构
// 一级缓存:完全初始化好的 Bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:早期暴露的半成品 Bean(属性尚未注入完毕)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:存放 ObjectFactory 工厂,用于生成早期 Bean 的代理对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
| 缓存级别 | 别名 | 存放内容 | 用途 |
|---|---|---|---|
| 一级 | singletonObjects | 完整初始化的 Bean 实例 | 最终供客户端使用 |
| 二级 | earlySingletonObjects | 半成品 Bean(已实例化,未完成属性注入) | 提前暴露给依赖方 |
| 三级 | singletonFactories | ObjectFactory 工厂 | 产生半成品或 AOP 代理对象 |
解决流程(A ↔ B 循环依赖)
1. 开始创建 A
├─ A 实例化(调用构造方法)→ 半成品
├─ 将 A 的 ObjectFactory 放入三级缓存
├─ 填充属性:发现需要 B
│ └─ 去缓存找 B → 找不到 → 开始创建 B
│
2. 开始创建 B
├─ B 实例化 → 半成品
├─ 将 B 的 ObjectFactory 放入三级缓存
├─ 填充属性:发现需要 A
│ └─ 去缓存找 A
│ ├─ 一级:没有
│ ├─ 二级:没有
│ └─ 三级:找到 A 的 ObjectFactory → 执行 getObject()
│ ├─ 如果 A 需要 AOP:返回代理对象
│ └─ 否则返回原始半成品
│ → 将结果放入二级缓存,从三级缓存移除
│ → B 成功注入 A(此时 A 还是半成品)
├─ B 初始化完成 → 放入一级缓存
│
3. 回到 A
├─ A 成功注入 B
├─ A 初始化完成 → 放入一级缓存
└─ 从二级/三级缓存移除
为什么需要三级,两级不够?
关键原因:AOP(动态代理)的生命周期管理。
- 如果只有两级缓存,Bean 在实例化后必须马上创建代理对象放入二级缓存,但 Spring 的设计原则是 AOP 代理应在 Bean 初始化阶段末尾通过
BeanPostProcessor创建。 - 三级缓存引入
ObjectFactory,实现了延迟创建代理:没有循环依赖时,三级缓存不会被触发,AOP 代理按正常生命周期在初始化后创建;只有发生循环依赖时,依赖方才会触发三级缓存提前生成代理。
一句话:三级缓存不是必需的,AOP 才是三级缓存的原因。
4.4 @Lazy 原理
@Lazy 是 Spring 提供的延迟加载机制,分为两种场景:
场景一:单独使用
@Component
@Lazy
public class HeavyService {
public HeavyService() {
// 容器启动时不创建,首次 getBean 时才创建
}
}
Spring 扫描到 @Lazy 后,在容器刷新的最后阶段(finishBeanFactoryInitialization)会跳过该 Bean 的提前实例化,等到代码中第一次调 getBean() 或注入时才触发创建。
场景二:注入时使用(动态代理)
@Component
public class AService {
@Autowired
@Lazy
private BService bService; // B 是懒加载的
}
此时 A 需要注入 B,但 B 又是懒加载不能现在创建,Spring 的做法是:
- 不为 B 创建真实对象,而是用 CGLIB 或 JDK 动态代理生成一个 B 的空壳代理对象
- 把这个代理对象注入给 A 的
bService字段 - 当 A 执行
bService.doSomething()时,代理拦截方法调用 - 代理去容器中检查 B 是否已创建,若未创建则触发
getBean("bService")完成真实创建 - 将方法调用转发给真实的 B 实例
这是"善意的欺骗"——注入的是一个代理壳子,把对象的真正创建延迟到了方法首次被调用的瞬间。
@Lazy 解决构造器循环依赖
三级缓存只能解决 Setter/属性注入 的循环依赖。对于构造器注入的循环依赖,三级缓存也无力回天(因为构造器调用时 Bean 还没实例化,放不进三级缓存)。
此时 @Lazy 可以破局:
@Component
public class AService {
public AService(@Lazy BService bService) { // 构造器注入 + @Lazy
this.bService = bService;
}
}
Spring 看到 @Lazy 后,直接注入一个 B 的代理壳子到构造器,B 本身并不创建。A 成功实例化,循环依赖破解。
5. 单例模式生态应用
单例模式的核心理念——全局唯一、资源共享——在 Java 生态中无处不在。
5.1 JDK 源码中的经典单例
// Runtime —— 饿汉式单例
public class Runtime {
private static final Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
| JDK 类 | 实现方式 | 用途 |
|---|---|---|
Runtime.getRuntime() | 饿汉式 | 获取 Java 运行时环境 |
Desktop.getDesktop() | 双检锁(DCL) | 获取系统桌面(浏览器/邮件) |
System.console() | 单次检查 | 获取控制台对象 |
5.2 日志框架
private static final Logger log = LoggerFactory.getLogger(XxxService.class);
LoggerFactory 内部通过单例注册表模式维护 LoggerContext:
- 同一个 Class 对应的
Logger实例全局唯一 - 保证日志级别变更能全局同步
- Logback、Log4j2 均采用此设计
5.3 数据库连接池
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/db")
.build();
}
- 连接池本身(HikariCP、Druid) 在 Spring 容器中是单例 Bean
- 如果不是单例,会创建多个连接池,耗尽数据库连接资源
- 连接池内部管理的
Connection对象则可以有多条(池化复用)
5.4 中间件客户端
| 中间件 | 单例对象 | 原因 |
|---|---|---|
| Redis | JedisPool / LettuceConnectionFactory | 管理网络长连接,全局唯一 |
| RocketMQ | DefaultMQProducer | 与 Broker 搭建复杂路由,高代价初始化 |
| RabbitMQ | ConnectionFactory | 维护连接池,防止资源浪费 |
| ZooKeeper | ZooKeeper 客户端实例 | 维护会话状态,全局一致性 |
5.5 Spring 框架内部
| 组件 | 角色 |
|---|---|
ApplicationContext | 全局唯一的 IoC 容器上下文 |
Environment | 管理所有配置来源(properties、环境变量) |
BeanPostProcessor | 干预所有 Bean 生命周期的全局处理器 |
6. 总结
核心要点
- 单例模式的本质:控制一个类在特定范围内的实例数量为 1,提供全局访问点
- 经典单例 vs Spring 单例:前者在 ClassLoader 级别通过私有构造实现,后者在 IoC 容器级别通过注册表缓存实现
- 六种实现推荐顺序:静态内部类 ≈ 枚举 > 饿汉式 > DCL > 懒汉式(同步) > 懒汉式(非同步)
选型建议
| 场景 | 推荐实现 |
|---|---|
| 常规 Java 项目 | 静态内部类 |
| 需要序列化/反射防护 | 枚举 |
| 无延迟加载需求、追求简单 | 饿汉式 |
| JDK 源码场景(如 Runtime) | 饿汉式 |
| Spring 管理 Bean | 直接用 @Component + @Scope("singleton"),不要手写单例 |
最终一句话
在现代 Java 企业级开发中,尽量少自己动手写经典单例模式。把对象的生命周期和单例控制交给 Spring IoC 容器 去管理,既能享受单例带来的资源节约,又能保持代码的松耦合和高可测性。理解单例模式的价值在于读懂框架源码,而不是在业务代码中四处手动实现它。
更多推荐
所有评论(0)