单例模式
单例模式:设计模式中的全局唯一实例解决方案
一、单例模式的定义与核心意图
单例模式(Singleton Pattern)是创建型设计模式的典型代表,其核心目标可概括为两点:
-
实例唯一性:确保在整个应用程序的生命周期内,某个类只能创建出一个实例对象,避免因多实例导致的资源冲突或状态不一致问题。
-
全局可访问性:为该唯一实例提供一个统一的、全局的访问入口,无需频繁创建和销毁对象,降低系统性能开销。
从设计本质来看,单例模式通过控制对象的创建流程,将类的实例化权限 “收归己有”,从而
现对资源的集中管理。例如,数据库连接池、日志管理器等场景,若允许创建多个实例,不仅会浪费内存资源,还可能导致数据同步异常,这正是单例模式的核心应用价值所在。
二、单例模式的设计原理与 UML 类图
1. 核心设计原理
单例模式的实现依赖三个关键技术点,缺一不可:
-
私有化构造方法:通过
private修饰构造方法,禁止外部类通过new关键字直接创建实例,从根源上控制实例的创建渠道。实 -
私有静态成员变量:在类内部定义一个
private static修饰的成员变量,用于存储该类的唯一实例,确保实例与类本身绑定(而非与对象绑定)。 -
公有静态访问方法:提供一个
public static修饰的方法(通常命名为getInstance()),作为全局访问入口,负责创建实例(若未创建)并返回该实例。
2. UML 类图
单例模式的类结构简洁清晰,以下是标准 UML 类图:
从类图可见,单例类不依赖其他类,仅通过自身静态成员和方法完成实例的控制与访问,符合 “高内聚” 的设计原则。
三、单例模式的多种实现方式详解
单例模式的实现方式多样,不同方式在懒加载、线程安全、性能等维度各有优劣,需根据实际场景选择。以下是常用实现方式的对比与代码示例:
1. 饿汉式(Eager Initialization)
核心特点
-
初始化时机:类加载时(即 JVM 加载该类到内存时)立即创建实例,属于 “饿加载”,无需等待首次调用。
-
线程安全:天生线程安全(JVM 保证类加载过程是线程安全的,且静态变量初始化仅执行一次)。
-
性能:类加载阶段会占用内存,若实例未被使用,会造成资源浪费。
代码实现
public class EagerSingleton {
// 1. 私有静态成员变量:类加载时直接初始化,确保唯一
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 2. 私有化构造方法:禁止外部创建实例
private EagerSingleton() {
// 可选:防止通过反射破坏单例(后续章节详解)
if (INSTANCE != null) {
throw new RuntimeException("单例模式禁止重复创建实例");
}
}
// 3. 公有静态访问方法:返回唯一实例
public static EagerSingleton getInstance() {
return INSTANCE;
}
// 业务方法示例
public void printInfo() {
System.out.println("饿汉式单例:当前实例哈希值=" + this.hashCode());
}
}
适用场景
适合实例创建成本低、且程序启动后必然会使用的场景(如系统配置管理器)。
2. 懒汉式(Lazy Initialization)
核心特点
-
初始化时机:首次调用
getInstance()方法时才创建实例,属于 “懒加载”,避免资源浪费。 -
线程安全:基础版线程不安全,需通过同步机制优化。
(1)基础版(线程不安全)
问题:多线程环境下,若多个线程同时进入 if (instance == null) 判断,会创建多个实例,破坏单例特性。
public class LazySingletonUnsafe {
private static LazySingletonUnsafe instance;
private LazySingletonUnsafe() {}
// 线程不安全:多线程并发时可能创建多个实例
public static LazySingletonUnsafe getInstance() {
if (instance == null) {
// 线程A、B同时进入此处,会创建两个实例
instance = new LazySingletonUnsafe();
}
return instance;
}
}
(2)同步方法版(线程安全但性能低)
优化:给 getInstance() 方法加 synchronized 关键字,确保同一时间只有一个线程执行该方法。
问题:每次调用 getInstance() 都会加锁,即使实例已创建,仍会产生锁竞争,导致性能下降。
public class LazySingletonSynchronized {
private static LazySingletonSynchronized instance;
private LazySingletonSynchronized() {}
// 线程安全:但整个方法加锁,性能开销大
public static synchronized LazySingletonSynchronized getInstance() {
if (instance == null) {
instance = new LazySingletonSynchronized();
}
return instance;
}
}
(3)双重检查锁(DCL,推荐)
优化:将锁粒度缩小到 “实例创建代码块”,并通过两次 if (instance == null) 检查,兼顾线程安全与性能。
关键细节:需用 volatile 关键字修饰 instance,防止 JVM 指令重排(new 操作分三步:分配内存→初始化对象→赋值给变量,指令重排可能导致 “变量已赋值但对象未初始化”,其他线程获取到不完整实例)。
public class LazySingletonDCL {
// volatile:防止指令重排,确保实例初始化完整后才被访问
private static volatile LazySingletonDCL instance;
private LazySingletonDCL() {
// 防止反射破坏
if (instance != null) {
throw new RuntimeException("单例模式禁止重复创建实例");
}
}
// 双重检查锁:高效且线程安全
public static LazySingletonDCL getInstance() {
// 第一次检查:实例已创建则直接返回,避免进入同步块(提升性能)
if (instance == null) {
// 类级别的锁:确保同一时间只有一个线程进入
synchronized (LazySingletonDCL.class) {
// 第二次检查:防止多个线程等待锁时重复创建实例
if (instance == null) {
instance = new LazySingletonDCL();
}
}
}
return instance;
}
}
适用场景
适合实例创建成本高、且不一定会被使用的场景(如大型文件解析器)。
3. 静态内部类(Static Inner Class)
核心特点
-
懒加载:静态内部类
SingletonHolder仅在getInstance()被调用时才会加载,避免资源浪费。 -
线程安全:JVM 保证静态内部类的初始化过程是线程安全的,且静态变量
INSTANCE仅初始化一次。 -
性能:无需加锁,性能优于双重检查锁。
代码实现
public class InnerClassSingleton {
// 1. 私有化构造方法
private InnerClassSingleton() {
// 防止反射破坏
if (SingletonHolder.INSTANCE != null) {
throw new RuntimeException("单例模式禁止重复创建实例");
}
}
// 2. 静态内部类:仅在调用getInstance()时加载
private static class SingletonHolder {
// 静态变量:JVM保证初始化过程线程安全
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
// 3. 公有静态访问方法
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
适用场景
大多数场景下的首选方案,兼顾懒加载、线程安全与性能。
4. 枚举单例(Enum Singleton)
核心特点
-
《Effective Java》推荐:天然解决线程安全、反射破坏、序列化破坏三大问题。
-
简洁性:代码量极少,无需手动处理复杂逻辑。
-
局限性:无法实现懒加载(枚举类加载时即初始化实例)。
代码实现
public enum EnumSingleton {
// 唯一实例(枚举常量默认是public static final的)
INSTANCE;
// 可选:添加成员变量存储状态
private String config;
// 枚举构造方法(默认private,且只能是private)
EnumSingleton() {
// 初始化操作:如加载配置文件
this.config = "系统默认配置";
}
// 业务方法示例
public void updateConfig(String newConfig) {
this.config = newConfig;
}
public String getConfig() {
return this.config;
}
}
为什么枚举能防止反射与序列化?
-
反射防护:JVM 明确规定,无法通过反射创建枚举类的实例(
Constructor.newInstance()会抛出IllegalArgumentException)。 -
序列化防护:枚举类默认实现
Serializable接口,且 JVM 保证反序列化时不会创建新实例,而是直接返回已存在的枚举常量。
适用场景
对安全性要求极高、且可接受非懒加载的场景(如权限管理器、加密工具类)。
四、单例模式的安全性问题与防御措施
单例模式的 “唯一性” 可能被反射和序列化破坏,需针对性防御:
1. 反射攻击与防御
反射破坏的原理
即使构造方法是 private,反射仍可通过 Constructor.setAccessible(true) 绕过访问权限检查,调用构造方法创建新实例。
破坏示例(以双重检查锁为例)
public class SingletonReflectionTest {
public static void main(String[] args) throws Exception {
// 1. 获取正常单例实例
LazySingletonDCL instance1 = LazySingletonDCL.getInstance();
// 2. 通过反射获取构造方法并创建新实例
Class<LazySingletonDCL> clazz = LazySingletonDCL.class;
Constructor<LazySingletonDCL> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true); // 绕过私有构造方法
LazySingletonDCL instance2 = constructor.newInstance();
// 3. 验证:两个实例不是同一个(单例被破坏)
System.out.println(instance1 == instance2); // 输出 false
}
}
防御措施
-
方案 1:在构造方法中添加检查,若实例已存在则抛出异常(如前文代码中 “防止反射破坏” 的逻辑)。
-
方案 2:使用枚举单例(JVM 天然防护,反射无法创建枚举实例)。
2. 序列化与反序列化问题
问题描述
若单例类实现 Serializable 接口,反序列化时(即从文件 / 网络读取对象时),JVM 会创建一个新实例,破坏单例特性。
破坏示例
public class SingletonSerializationTest {
public static void main(String[] args) throws Exception {
// 1. 序列化单例实例到文件
SerializableSingleton instance1 = SerializableSingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
oos.writeObject(instance1);
oos.close();
// 2. 从文件反序列化获取实例
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj"));
SerializableSingleton instance2 = (SerializableSingleton) ois.readObject();
ois.close();
// 3. 验证:两个实例不是同一个(单例被破坏)
System.out.println(instance1 == instance2); // 输出 false
}
}
防御措施
在单例类中添加 readResolve() 方法,反序列化时会调用该方法返回已存在的实例,而非创建新实例:
public class SerializableSingleton implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
// 关键:反序列化时返回已有实例
private Object readResolve() {
return INSTANCE;
}
}
五、单例模式的应用场景
单例模式的核心价值是 “资源复用” 与 “状态统一”,以下是典型应用场景:
-
资源密集型对象:数据库连接池、线程池、Redis 连接客户端等。这类对象创建成本高(如建立网络连接),频繁创建会导致性能瓶颈,单例模式可减少资源消耗。
-
全局状态管理:系统配置管理器、日志管理器、用户会话管理器等。需确保全局只有一个 “数据源”,避免多实例导致的配置不一致或日志错乱。
-
硬件资源控制:打印机管理器、显卡驱动管理器等。硬件设备通常只能被一个实例控制,多实例可能导致设备冲突。
-
工具类优化:若工具类创建时需加载大量静态资源(如字典数据),单例模式可避免重复加载,提升效率。
六、单例模式的优缺点
优点
-
节省资源:控制实例数量为 1,避免多实例占用内存和 CPU 资源。
-
全局访问:提供统一入口,简化代码调用,无需在多个地方传递实例。
-
状态稳定:确保全局状态一致,避免多实例修改同一资源导致的并发问题。
缺点
-
违背单一职责原则:单例类既要实现业务逻辑,又要负责实例的创建与管理,职责过重。
-
扩展性差:若后续需求变更(如需要多个实例),需修改单例类的核心逻辑,不符合 “开闭原则”。
-
测试困难:单例类依赖全局状态,单元测试时难以模拟不同场景(如实例已初始化后无法重置)。
-
并发风险(实现不当):若未处理好线程安全(如使用基础版懒汉式),会导致多实例问题。
-
破坏依赖注入:单例类通常通过静态方法获取,而非通过构造方法注入,增加代码耦合度。
七、单例模式的变种
除了标准实现,单例模式还有一些变种,适用于特殊场景:
1. 容器式单例
核心思想
通过一个 “容器”(如 Map)管理多个单例对象,按 “key” 获取对应实例,实现 “多单例” 管理。
代码实现
public class SingletonContainer {
// 容器:存储单例对象,key为类名或自定义标识
private static final Map<String, Object> SINGLETON_MAP = new ConcurrentHashMap<>();
private SingletonContainer() {}
// 注册单例:若不存在则创建并加入容器
public static <T> void registerSingleton(String key, Class<T> clazz) throws Exception {
if (!SINGLETON_MAP.containsKey(key)) {
T instance = clazz.getDeclaredConstructor().newInstance();
SINGLETON_MAP.put(key, instance);
}
}
// 获取单例:按key从容器中获取
public static <T> T getSingleton(String key) {
return (T) SINGLETON_MAP.get(key);
}
}
适用场景
需管理多个单例对象的场景(如系统中有多个配置文件,每个配置文件对应一个单例)。
2. 线程局部单例(ThreadLocal Singleton)
核心思想
为每个线程创建一个实例,确保 “线程内唯一”,而非 “全局唯一”。适用于多线程场景下,每个线程需独立实例的需求(如线程上下文、事务管理)。
代码实现
public class ThreadLocalSingleton {
// ThreadLocal:存储每个线程对应的实例
private static final ThreadLocal<ThreadLocalSingleton> THREAD_LOCAL =
ThreadLocal.withInitial(ThreadLocalSingleton::new);
private ThreadLocalSingleton() {}
// 获取当前线程的实例
public static ThreadLocalSingleton getInstance() {
return THREAD_LOCAL.get();
}
// 可选:移除当前线程的实例(避免内存泄漏)
public static void removeInstance() {
THREAD_LOCAL.remove();
}
}
特点
-
线程安全:每个线程操作自己的实例,无并发冲突。
-
内存注意:需在线程结束时调用
removeInstance(),避免ThreadLocal导致的内存泄漏。
八、单例模式的最佳实践总结
-
优先选择枚举单例:若可接受非懒加载,枚举单例是最安全的方案(天然防反射、防序列化),且代码简洁。
-
需要懒加载选静态内部类:兼顾懒加载、线程安全与性能,是大多数场景的 “最优解”。
-
复杂场景用双重检查锁:若需在实例创建时传入参数(静态内部类无法直接传参),可使用双重检查锁,但需注意
volatile关键字。 -
避免滥用单例:工具类若无需状态(如纯静态方法),无需设计为单例;简单对象(创建成本低)也无需单例。
-
处理特殊场景:需多单例用容器式,线程内唯一用 ThreadLocal,确保单例模式与需求匹配。
单例模式是设计模式中最基础也最容易被滥用的模式,关键在于 “权衡”—— 根据资源成本、并发需求、扩展性要求选择合适的实现方式,而非盲目使用。

浙公网安备 33010602011771号