深入浅出设计模式【一、单例模式】
一、单例模式介绍
单例模式是一种创建型设计模式,其核心在于确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
在软件系统中,经常存在这样的需求:一个类只需要一个实例来协调系统行为。例如,线程池、缓存、日志对象、对话框、打印机驱动对象等。如果这些类存在多个实例,会导致程序行为异常、资源使用过量,或者结果不一致。单例模式正是为解决此类问题而生的。
二、核心概念与意图
-
核心概念:
- 唯一实例: 控制实例的数量,禁止通过其他方式(如
new)创建该类的对象。 - 全局访问点: 提供一个众所周知的静态方法,让任何需要该实例的客户端代码都能轻松获取到它。
- 唯一实例: 控制实例的数量,禁止通过其他方式(如
-
意图:
- 保证一个类仅有一个实例。
- 提供对该实例的全局访问点。
三、适用场景剖析
单例模式不应滥用,其典型适用场景包括:
- 控制资源访问: 当需要严格控制和统一管理共享资源时。例如,数据库连接池、线程池、缓存系统等。多个实例会导致资源耗尽或状态混乱。
- 管理全局状态: 当需要一个对象来存储整个应用程序的全局状态或配置信息时。例如,应用的配置管理器(
ConfigManager),所有组件都应从同一个地方读取配置。 - 充当中央通信枢纽: 当对象需要充当一个中央通信点,协调多个不同部分的操作时。例如,日志记录器(
Logger),所有模块都将日志信息发送到同一个日志器实例进行处理和输出。 - 工厂对象本身: 如果工厂类本身不包含需要变化的状态,它也可以被设计成单例,例如抽象工厂模式中的具体工厂类。
不适用场景:
- 如果对象本身是有状态的且状态可能随客户端变化,使用单例会导致意外的副作用(隐含的耦合)。
- 在需要大量测试的场景中,单例的全局状态会使单元测试变得困难,因为它难以被隔离和模拟(Mock)。
四、UML 类图解析
单例模式的UML类图非常简单,但内涵明确。
Singleton类:- 私有静态成员 (
- static instance: Singleton): 用于保存类的唯一实例。 - 私有构造函数 (
- Singleton()): 这是实现单例的关键。将构造函数设为私有,防止外部代码通过new Singleton()的方式创建实例。这是对“控制实例创建”的直接体现。 - 公有静态方法 (
+ static getInstance(): Singleton): 这是全局访问点。该方法负责检查实例是否已存在。如果不存在,则创建它;如果已存在,则返回已有的实例。这是对“提供全局访问点”的直接体现。
- 私有静态成员 (
五、各种实现方式及其优缺点
单例的实现方式多样,其演进史也反映了对线程安全、性能、语义清晰度的不断追求。
1. 饿汉式 (Eager Initialization)
在类加载时就立即初始化实例。
public class EagerSingleton {
// 1. 私有静态实例,类加载时即初始化
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 2. 私有构造函数
private EagerSingleton() {}
// 3. 公有静态方法,返回唯一实例
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
- 优点:
- 实现简单。
- 线程安全: 利用类加载机制保证线程安全(JVM在加载类时是互斥的)。
- 缺点:
- 可能造成资源浪费: 如果这个实例非常耗费资源,但在整个程序生命周期中从未被使用过,就造成了不必要的开销。
- 无法延迟加载 (Lazy Loading)。
2. 懒汉式 (Lazy Initialization) - 非线程安全版
只有在第一次调用 getInstance() 时才创建实例。
public class UnsafeLazySingleton {
private static UnsafeLazySingleton instance;
private UnsafeLazySingleton() {}
public static UnsafeLazySingleton getInstance() {
// 非线程安全:多线程环境下可能创建多个实例
if (instance == null) {
instance = new UnsafeLazySingleton();
}
return instance;
}
}
- 优点:
- 实现了延迟加载。
- 缺点:
- 线程不安全: 在多线程环境下,多个线程可能同时进入
if (instance == null)判断,从而创建多个实例。绝对不可用于生产环境。
- 线程不安全: 在多线程环境下,多个线程可能同时进入
3. 懒汉式 - 线程安全版 (Synchronized Method)
通过给方法加锁来解决线程安全问题。
public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton instance;
private SynchronizedLazySingleton() {}
// 使用 synchronized 关键字,保证方法级别的同步
public static synchronized SynchronizedLazySingleton getInstance() {
if (instance == null) {
instance = new SynchronizedLazySingleton();
}
return instance;
}
}
- 优点:
- 线程安全。
- 实现了延迟加载。
- 缺点:
- 性能低下: 每次调用
getInstance()都需要进行同步,而大部分情况下实例已经创建,同步就成了不必要的开销。
- 性能低下: 每次调用
4. 双重校验锁 (Double-Checked Locking - DCL)
在加锁前后进行两次检查,以减少同步的开销。
public class DCLSingleton {
// 使用 volatile 关键字禁止指令重排序,这是JDK5之后的关键修正
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
// 第一次检查,避免不必要的同步
if (instance == null) {
synchronized (DCLSingleton.class) {
// 第二次检查,确保在同步块内实例仍为null
if (instance == null) {
instance = new DCLSingleton(); // 1. 分配内存 2. 初始化 3. 引用赋值
// volatile 防止步骤2和3被重排序,保证其他线程看到的是完全初始化的对象
}
}
}
return instance;
}
}
- 优点:
- 线程安全。
- 延迟加载。
- 性能较高: 只有在第一次创建实例时才会同步。
- 缺点:
- 实现稍复杂。
- 需要理解
volatile和指令重排序的内存模型概念。
5. 静态内部类 (Static Inner Class / Holder Class)
利用JVM的类加载机制来实现懒加载和线程安全,这是推荐的一种实现方式。
public class InnerClassSingleton {
private InnerClassSingleton() {}
// 静态内部类
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
// 首次调用getInstance()时,JVM才会加载SingletonHolder类并初始化INSTANCE
return SingletonHolder.INSTANCE;
}
}
- 优点:
- 线程安全: 由JVM保证。
- 延迟加载: 只有在调用
getInstance()时,内部类才会被加载,实例才会被创建。 - 实现简洁: 无需
synchronized或volatile,代码优雅。
- 缺点:
- 无法防止通过反射或反序列化创建新实例。
6. 枚举 (Enum)
《Effective Java》作者Joshua Bloch极力推荐的方式。
public enum EnumSingleton {
INSTANCE; // 唯一的实例
// 可以添加方法
public void doSomething() {
System.out.println("Doing something with " + this);
}
}
// 使用:EnumSingleton.INSTANCE.doSomething();
- 优点:
- 绝对防止多次实例化: 即使面对复杂的反射和序列化攻击,也能保证单例。这是由JVM在底层保证的。
- 线程安全。
- 实现极其简单。
- 缺点:
- 不适用于需要继承的场景(因为枚举隐式继承自
Enum)。 - 不够灵活(例如无法实现延迟加载,虽然这在枚举单例中通常不是问题)。
- 不适用于需要继承的场景(因为枚举隐式继承自
六、最佳实践
- 首选枚举实现: 如果你的场景适用(不需要延迟加载、不继承其他类),枚举是实现单例的最佳方式,因为它简单、安全、无敌。
- 次选静态内部类实现: 如果需要延迟加载,并且不介意潜在的反射/反序列化漏洞,静态内部类是实现单例的最佳方式。它简洁、高效、安全。
- 明确需求: 在决定使用单例前,反复问自己是否真的只需要一个实例。单例本质上是一种“优雅的全局变量”,要警惕其对代码可测试性和耦合性的影响。
- 考虑依赖注入 (DI): 在现代框架(如Spring)中,容器管理的Bean默认就是单例。优先使用框架的IoC容器来管理单例,而不是自己实现。这样既能享受单例的好处,又能避免手动实现的问题,并且便于测试。
- 防御反射和序列化攻击:
- 反射: 在私有构造函数中添加检查,如果实例已存在则抛出异常。
private Singleton() { if (instance != null) { throw new IllegalStateException("Instance already exists"); } }- 序列化: 如果单例类实现了
Serializable,必须提供readResolve()方法以防止反序列化时创建新对象。
protected Object readResolve() { return getInstance(); }
七、在开发中的演变和应用
单例模式的概念随着编程范式和技术的发展而演变:
- 从手动实现到容器托管: 早期Java开发中,开发者需要手动实现上述各种单例。如今,在Spring等IoC框架中,通过
@Component或@Bean注解,并指定作用域为singleton,容器会自动创建并管理单例Bean的生命周期。这是单例思想在架构层面的应用和升华。 - 单例与多例: 单例模式催生了“多例模式”(Multiton)的概念,即一个类有有限个实例(如连接池中的多个连接),并通过一个键(Key)来获取。
- 与工具类的区别: 工具类(
Math,Arrays)只有静态方法,没有状态,不需要实例化。单例类通常有状态(如配置信息、缓存数据),虽然只有一个实例,但它是一个真正的对象。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
Java Runtime 类:
Runtime runtime = Runtime.getRuntime();Runtime类封装了Java应用运行时的环境,每个Java应用程序都有一个唯一的Runtime实例。这是典型的饿汉式单例。 -
Java Desktop 类:
Desktop desktop = Desktop.getDesktop();用于与桌面环境交互,如打开文件、打印等。也是单例。
-
Spring Framework:
Spring容器中,默认情况下,由容器创建和管理的Bean都是单例的。这是单例模式在企业级开发中最广泛、最成功的应用。@Service // 默认就是单例 public class UserServiceImpl implements UserService { // ... } -
日志框架:
Logback / Log4j 中的Logger实例通常按名称(通常是类名)来获取,对同一个名称的Logger的获取总是返回同一个实例,这本质上是一种单例注册表(Registry)的实现,类似于多例模式。Logger logger = LoggerFactory.getLogger(MyClass.class); -
工具类:
虽然工具类本身不是单例模式(因为它们没有实例),但其思想与单例“全局唯一访问点”的意图相通。例如java.util.Collections。
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 创建型设计模式 |
| 核心意图 | 保证一个类只有一个实例,并提供全局访问点 |
| 关键实现 | 1. 私有构造函数 2. 私有静态实例引用 3. 公有静态获取方法 |
| 主要优点 | 1. 严格控制实例数量,节约系统资源 2. 提供一致的全局访问点,避免混乱 |
| 主要缺点 | 1. 违反了单一职责原则(既管理实例又承担业务) 2. 代码耦合性高,不利于测试和扩展 3. 在分布式或并行环境中,可能需要特殊处理 |
| 适用场景 | 需要严格管理共享资源或全局状态的场景,如配置、连接池、日志、工厂等 |
| 实现推荐 | 简单安全首选枚举,延迟加载首选静态内部类。现代开发中优先使用IoC容器托管。 |
单例模式是一个简单而强大的模式,但其“全局状态”的特性是一把双刃剑。在现代软件开发中,应谨慎使用手动实现的单例,更多地将其思想与依赖注入容器相结合,从而在获得单例好处的同时,保持代码的松散耦合和可测试性。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120433

浙公网安备 33010602011771号