Java 设计模式项目 Singleton 单例模式全解析:从枚举实现到线程安全实战
Java 设计模式项目 Singleton 单例模式全解析:从枚举实现到线程安全实战
导读
本文基于 java-design-patterns 仓库的 Singleton(单例模式)模块,系统讲解这一经典创建型模式的核心思想、适用场景与代价权衡,并深入剖析仓库中 6 种单例实现方案(饿汉式、枚举式、同步懒加载、双重检查锁、Holder 延迟占位、Bill Pugh 实现)的源码结构与线程安全性差异。读完本文,你将掌握每种实现的内在原理、如何用仓库提供的测试验证单例唯一性,以及在实际项目中如何根据并发与初始化时机要求选择最合适的实现。
模式定位:什么是 Singleton
Singleton 是 GoF(Gang of Four)四大经典创建型模式之一,该模块在仓库中的定位信息见 localization/es/singleton/README.md 的元数据(category: Creational,tag: Gang of Four)。
目的(Propósito)
确保一个类在整个系统中只存在一个实例,并提供一个全局访问点供所有客户端使用。
现实世界的类比:魔法象牙塔
原文档用了一个生动的比喻:在一座魔法世界中,巫师们学习魔法时只能使用同一座被魔法环绕的象牙塔。无论多少巫师前来,象牙塔始终只有一座——这座象牙塔就是单例。
用一句话概括:确保某个特定类只会被创建出一个对象。
Wikipedia 的定义与仓库实现一致:
在软件工程中,单例模式是一种软件设计模式,它将类的实例化限制为单个对象。当整个系统恰好需要一个对象来协调动作时,这一模式非常有用。
单例的三个基本要素
从 singleton/src/main/java/com/iluwatar/singleton/ 的源码可以归纳出所有单例实现共享的三个要素:
- 私有构造器:防止外部通过
new直接实例化; - 静态实例字段:持有唯一的实例引用;
- 静态访问方法(
getInstance()):全局访问点。
以最简单的饿汉式实现 IvoryTower.java 为例,这三要素清晰可见:
public final class IvoryTower {
/** Private constructor so nobody can instantiate the class. */
private IvoryTower() {
// to prevent instantiating by Reflection call
if (INSTANCE != null) {
throw new IllegalStateException("Already initialized.");
}
}
/** Static to class instance of the class. */
private static final IvoryTower INSTANCE = new IvoryTower();
public static IvoryTower getInstance() {
return INSTANCE;
}
}
注意:仓库中的构造器在 INSTANCE != null 时会抛出 IllegalStateException,这是对反射攻击的防御——即便通过反射调用私有构造器,也无法创建出第二个实例。
程序化示例:枚举单例(Effective Java 推荐方案)
原文档引用了 Joshua Bloch《Effective Java》第 2 版第 18 页的经典结论:
单元素枚举类型是实现单例的最佳方式。
仓库中对应的实现位于 EnumIvoryTower.java,代码极简:
public enum EnumIvoryTower {
INSTANCE
}
在 App.java 的入口演示中,通过两次访问 EnumIvoryTower.INSTANCE 并打印其对象标识:
var enumIvoryTower1 = EnumIvoryTower.INSTANCE;
var enumIvoryTower2 = EnumIvoryTower.INSTANCE;
LOGGER.info("enumIvoryTower1={}", enumIvoryTower1);
LOGGER.info("enumIvoryTower2={}", enumIvoryTower2);
控制台输出(两次打印的对象哈希一致,证明是同一实例):
enumIvoryTower1=com.iluwatar.singleton.EnumIvoryTower@1221555852
enumIvoryTower2=com.iluwatar.singleton.EnumIvoryTower@1221555852
仓库中的 EnumIvoryTower.java 还重写了 toString(),返回 getDeclaringClass().getCanonicalName() + "@" + hashCode(),这正是输出中 com.iluwatar.singleton.EnumIvoryTower@1221555852 的来源。
为什么枚举单例是最佳方案? 从源码注释(见 EnumIvoryTower.java)与 JVM 机制可以总结:
- 线程安全:枚举常量的初始化由 JVM 保证,天然线程安全;
- 防反射:Java 语言规范(JLS 8.9)禁止对枚举进行反射实例化,仓库测试 EnumIvoryTowerTest.java 专门验证了这一点——调用
getDeclaredConstructor会直接抛出ReflectiveOperationException; - 防序列化破坏:枚举的序列化由 JVM 特殊处理,反序列化不会创建新实例。
当然,枚举方案也有代价:一旦选择枚举就无法再通过继承扩展,且所有附加方法的线程安全需要开发者自行负责。
类图与结构
仓库为该模式提供了完整的 UML 类图与序列图。类图展示了全部 6 种实现类与 App 入口的关系,见 singleton/etc/singleton.urm.png(其 PlantUML 源文件为 singleton/etc/singleton.urm.puml):
从 singleton.urm.puml 可以看出,每种实现都通过私有静态字段自引用(如 IvoryTower --> "-INSTANCE" IvoryTower),且 InitializingOnDemandHolderIdiom 通过 HelperHolder 内部静态类持有实例。
模块的调用时序可参考 singleton/etc/singleton-sequence-diagram.png:
仓库中的 6 种单例实现方案
原文档仅展示了枚举实现,而仓库 singleton/src/main/java/com/iluwatar/singleton/ 实际收录了 6 种不同取舍的单例实现,入口 App.java 逐一演示了它们。下面逐一剖析。
1. 饿汉式:IvoryTower(立即初始化,线程安全)
public final class IvoryTower {
private IvoryTower() { /* 防反射检查 */ }
private static final IvoryTower INSTANCE = new IvoryTower();
public static IvoryTower getInstance() { return INSTANCE; }
}
类加载时即创建实例,static final 字段由 JVM 保证只初始化一次,天然线程安全。代价是失去对实例化时机的控制——即使从未使用也会占用内存。适用场景是能接受「启动即创建」的简单场景。源码见 IvoryTower.java。
2. 枚举式:EnumIvoryTower(最简洁的线程安全实现)
如上节所述,单元素枚举即单例,代码只有一行常量声明。源码见 EnumIvoryTower.java。
3. 同步懒加载:ThreadSafeLazyLoadedIvoryTower
public final class ThreadSafeLazyLoadedIvoryTower {
private static volatile ThreadSafeLazyLoadedIvoryTower instance;
private ThreadSafeLazyLoadedIvoryTower() { /* 防反射检查 */ }
public static synchronized ThreadSafeLazyLoadedIvoryTower getInstance() {
if (instance == null) {
instance = new ThreadSafeLazyLoadedIvoryTower();
}
return instance;
}
}
实例在首次调用 getInstance() 时才创建(懒加载),整个访问方法用 synchronized 保证线程安全。其缺点也源于此:每次访问都要进入同步块,性能开销大。源码见 ThreadSafeLazyLoadedIvoryTower.java,instance 声明为 volatile 以保障多线程下的原子可见性。
4. 双重检查锁(Double-Checked Locking):ThreadSafeDoubleCheckLocking
public static ThreadSafeDoubleCheckLocking getInstance() {
var result = instance;
if (result == null) {
synchronized (ThreadSafeDoubleCheckLocking.class) {
result = instance;
if (result == null) {
result = new ThreadSafeDoubleCheckLocking();
instance = result;
}
}
}
return result;
}
仅在实例未初始化时才进入同步块,之后的访问完全无锁,性能优于方案 3。仓库源码 ThreadSafeDoubleCheckLocking.java 中有两点值得注意的实现细节:
- 使用局部变量
result暂存实例,源码注释引用《Effective Java》第 2 版 p.283-284,说明这样可将性能提升约 25%; instance必须为volatile,否则在 Java 1.4 及更早版本中该模式会被破坏(源码注释明确标注 "Broken under Java 1.4"),这是由 JMM 内存可见性语义决定的。
5. Holder 延迟占位惯用法:InitializingOnDemandHolderIdiom
public final class InitializingOnDemandHolderIdiom {
private InitializingOnDemandHolderIdiom() { /* 防反射检查 */ }
public static InitializingOnDemandHolderIdiom getInstance() {
return HelperHolder.INSTANCE;
}
private static class HelperHolder {
private static final InitializingOnDemandHolderIdiom INSTANCE =
new InitializingOnDemandHolderIdiom();
}
}
这是无需 synchronized 与 volatile 的线程安全懒加载方案。原理:内部静态类 HelperHolder 只有在 getInstance() 首次被调用、JVM 首次加载该类时才会初始化其静态字段,而 JVM 保证类的初始化是线程安全的(Java 语言规范对类初始化的保证)。因此该实现"尽可能懒、且在所有已知 Java 版本中均正确工作"(见源码注释 InitializingOnDemandHolderIdiom.java)。
6. Bill Pugh 实现:BillPughImplementation
public final class BillPughImplementation {
private BillPughImplementation() { /* 防反射检查 */ }
private static class InstanceHolder {
private static BillPughImplementation instance = new BillPughImplementation();
}
public static BillPughImplementation getInstance() {
return InstanceHolder.instance;
}
}
与方案 5 原理一致,同样是借助 JVM 类初始化机制实现懒加载 + 线程安全:在 getInstance() 被调用前,InstanceHolder 类不会被加载进内存,也不会为静态实例分配内存。源码注释见 BillPughImplementation.java。
实现方案横向对比
| 实现 | 初始化时机 | 线程安全 | 同步开销 | 防反射 | 关键代码 |
|---|---|---|---|---|---|
| IvoryTower | 饿汉(类加载时) | 是(JVM 保证) | 无 | 是(构造器检查) | IvoryTower.java |
| EnumIvoryTower | 枚举常量加载时 | 是(JVM 保证) | 无 | 是(JLS 禁止) | EnumIvoryTower.java |
| ThreadSafeLazyLoadedIvoryTower | 懒加载 | 是(方法级锁) | 每次访问 | 是 | ThreadSafeLazyLoadedIvoryTower.java |
| ThreadSafeDoubleCheckLocking | 懒加载 | 是(双重检查) | 仅首次 | 是 | ThreadSafeDoubleCheckLocking.java |
| InitializingOnDemandHolderIdiom | 懒加载(Holder 触发) | 是(类初始化机制) | 无 | 是 | InitializingOnDemandHolderIdiom.java |
| BillPughImplementation | 懒加载(Holder 触发) | 是(类初始化机制) | 无 | 是 | BillPughImplementation.java |
选型建议:需要最简洁且兼顾序列化/反射安全的场景首选枚举;能接受启动即初始化选饿汉式;既要懒加载又要高性能的并发场景,推荐 Holder 惯用法或 Bill Pugh 实现;而方法级同步的 ThreadSafeLazyLoadedIvoryTower 仅适合低并发访问场景。
测试如何验证单例唯一性
仓库的测试体系值得借鉴。抽象基类 SingletonTest.java 定义了三个核心测试维度,所有实现(如 IvoryTowerTest.java、ThreadSafeDoubleCheckLockingTest.java 等)均继承并复用:
- 同线程唯一性:同一线程内多次调用
getInstance(),用assertSame断言返回同一对象; - 多线程唯一性:创建 10000 个并发任务,在 8 线程固定线程池中同时调用
getInstance()(限时 10 秒),断言所有线程拿到的都是同一个实例; - 反射防御性:通过反射
setAccessible(true)调用私有构造器,断言抛出InvocationTargetException。
枚举单例的测试 EnumIvoryTowerTest.java 对反射测试做了特化——Java 不允许枚举实例化,直接断言 getDeclaredConstructor 抛出 ReflectiveOperationException。
这组测试同时印证了上文所有实现的线程安全与反射防御结论,是验证单例质量的标准范式。
适用场景(Aplicabilidad)
原文档明确了使用 Singleton 的时机:
- 某个类必须恰好只有一个实例,且客户端需要从一个众所周知的访问点获取它;
- 唯一实例应可通过继承扩展,且客户端无需修改代码即可使用扩展后的实例。
典型用例包括:
- 日志类(Logging class):整个应用共享同一日志输出通道;
- 数据库连接管理(Managing a connection to a database):连接池需要全局唯一的管理入口;
- 文件管理器(File manager):对文件系统资源的统一访问控制。
JDK 中的真实使用
仓库原文档列出的 JDK 标准库示例(同样适用于 Java 8 及以上版本):
java.lang.Runtime#getRuntime():JVM 运行时环境的唯一入口;java.awt.Desktop#getDesktop():桌面系统交互的唯一入口;java.lang.System#getSecurityManager():安全管理器的全局访问。
这些 JDK 方法全部返回单例对象,印证了"恰好一个实例 + 全局访问点"的标准形态。
代价与权衡(Consecuencias)
原文档明确列出 Singleton 的四大负面后果,务必在使用前评估:
- 违反单一职责原则(SRP):单例类既要承担自身业务职责,又要控制自己的创建与生命周期;
- 阻碍资源释放:鼓励全局共享实例,使得对象及其占用的资源难以被回收释放;
- 强耦合:Singleton 的客户端耦合到具体单例,变得难以测试(需要 mock 全局状态);
- 几乎无法继承:私有构造器与静态访问方式使子类化极为困难。
补充收益视角(源自英文版模块文档):单例提供了对唯一实例的受控访问、减少了命名空间污染、允许细化操作与表示、并且比静态类操作更灵活。核心风险始终是全局可变状态带来的测试困难与并发瓶颈——若在未仔细同步的并发环境中使用单例,可能成为系统瓶颈。
关联模式与延伸阅读
Singleton 常与其他创建型模式协同使用:抽象工厂(Abstract Factory)常被用来保证某个类只有唯一实例;工厂方法(Factory Method)可将单例的创建逻辑封装起来;原型(Prototype)模式避免重复创建实例,可与单例共同管理唯一实例。
原文档推荐的经典参考书(仓库模块的 Credits 部分):
- Design Patterns: Elements of Reusable Object-Oriented Software(GoF 原著)
- Effective Java(Joshua Bloch,第 2 版 p.18 关于枚举单例的论述)
- Head First Design Patterns: A Brain-Friendly Guide
- Refactoring to Patterns
若想在本地运行本文涉及的全部示例,可进入 singleton 模块,查看入口 App.java 并运行其 main 方法,观察 6 种单例的日志输出;运行 mvn test 可执行 src/test 下的全部单例测试。

