单例模式
单例模式(Singleton Pattern)详解
单例模式是创建型设计模式中最常用的模式之一,其核心是保证一个类在系统中仅有一个实例,并提供全局访问点。这种模式适用于需要统一控制资源、避免重复创建对象的场景(如配置管理器、日志工具等)。
单例模式的核心原则
实现单例模式需遵循四大原则:
- 构造方法私有:防止外部通过
new关键字创建实例。 - 以静态方法或枚举返回实例:提供全局访问接口。
- 确保实例唯一:通过设计避免多线程或其他场景下的重复创建。
- 防止反序列化重复实例:确保对象序列化 / 反序列化后仍为单例。
单例模式的两种核心实现方式
根据实例化时机的不同,单例模式分为饿汉式(立即加载)和懒汉式(延迟加载)。
1. 饿汉式(Eager Initialization)
核心思想
类加载时立即创建实例,通过类加载机制保证线程安全。
实现代码
public class EagerSingleton {
// 类加载时直接初始化实例
private static final EagerSingleton instance = new EagerSingleton();
// 私有构造方法,禁止外部实例化
private EagerSingleton() {}
// 全局访问点
public static EagerSingleton getInstance() {
return instance;
}
}
特点
- 线程安全:类加载过程由 JVM 保证线程安全,仅初始化一次。
- 立即加载:类加载时就创建实例,无论是否使用,可能浪费内存。
- 优点:实现简单,无需考虑线程同步问题。
- 缺点:如果实例占用资源大且长时间不使用,会造成资源浪费。
2. 懒汉式(Lazy Initialization)
核心思想
首次使用时才创建实例,节省资源(时间换空间),但需处理线程安全问题。
(1)基础版(非线程安全)
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
// 首次调用时创建实例
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
- 问题:多线程并发调用
getInstance()时,可能创建多个实例。
(2)同步方法版(线程安全,效率低)
public class SyncLazySingleton {
private static SyncLazySingleton instance;
private SyncLazySingleton() {}
// 对方法加锁,保证线程安全
public static synchronized SyncLazySingleton getInstance() {
if (instance == null) {
instance = new SyncLazySingleton();
}
return instance;
}
}
- 线程安全:
synchronized修饰方法,防止并发创建。 - 缺点:每次调用
getInstance()都需要加锁,性能开销大。
(3)双重检查锁(DCL,高效线程安全)
public class DCLSingleton {
// volatile防止指令重排导致的空指针问题
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
// 第一次检查:避免已创建实例后仍进入同步块
if (instance == null) {
// 同步块:限制仅一个线程进入实例化逻辑
synchronized (DCLSingleton.class) {
// 第二次检查:防止多线程等待锁时重复创建
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
- 核心优化:
- 外层
if:避免实例创建后仍执行同步操作,提高效率。 - 内层
if:防止多个线程同时进入同步块后重复创建实例。 volatile:禁止instance = new DCLSingleton()的指令重排(避免其他线程获取到未初始化完成的实例)。
- 外层
- 特点:线程安全且性能优异,是懒汉式的推荐实现。
(4)静态内部类版(线程安全,延迟加载)
public class StaticInnerClassSingleton {
// 静态内部类:仅在调用getInstance()时加载
private static class SingletonHolder {
// 内部类中创建实例
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
private StaticInnerClassSingleton() {}
public static StaticInnerClassSingleton getInstance() {
// 触发内部类加载,创建实例
return SingletonHolder.INSTANCE;
}
}
- 线程安全:内部类加载由 JVM 保证线程安全,仅初始化一次。
- 延迟加载:内部类不会随外部类加载而初始化,仅在首次调用
getInstance()时加载。 - 优点:实现简单,无需手动处理线程同步,效率高。
(5)枚举单例(防反射、防序列化)
上述实现均存在反射攻击(通过反射调用私有构造)和序列化问题(反序列化生成新实例),枚举单例可解决这些问题:
public enum EnumSingleton {
// 枚举实例(唯一)
INSTANCE;
// 自定义方法
public void doSomething() {
System.out.println("枚举单例执行操作");
}
}
- 使用方式:
EnumSingleton.INSTANCE.doSomething(); - 优势:
- 天然线程安全:枚举实例在类加载时创建,由 JVM 保证唯一性。
- 防反射:JVM 禁止反射创建枚举实例。
- 防序列化:枚举的序列化机制会保证反序列化后仍是原实例。
- 缺点:无法延迟加载(枚举实例在类加载时创建)。
单例模式的特点与适用场景
核心特点
- 单例类仅有一个实例。
- 单例类自行创建唯一实例。
- 提供全局访问点(如
getInstance())。
优点
- 节省资源:避免重复创建销毁对象,减少内存占用。
- 提高性能:频繁使用的对象(如数据库连接池)可减少初始化开销。
- 全局控制:便于统一管理共享资源(如配置信息、日志输出)。
缺点
- 扩展困难:单例类通常通过静态方法访问,无法通过继承扩展。
- 耦合性高:全局访问点可能导致代码依赖紧密,不利于测试。
- 潜在内存泄漏:单例生命周期与应用一致,若持有大量资源可能导致泄漏。
适用场景
- 资源共享场景:如日志工具、配置管理器、线程池。
- 控制资源访问场景:如数据库连接、文件读写(避免并发冲突)。
- 频繁创建销毁会消耗大量资源的对象:如重量级服务实例。
单例模式的注意事项
- 线程安全:懒汉式必须处理多线程并发问题(推荐 DCL 或静态内部类)。
- 反射与序列化:普通单例需额外处理(如添加
readResolve()方法防止反序列化重复),枚举单例天然免疫。 - 内存管理:避免单例持有过多资源或长生命周期对象,防止内存泄漏。
- 测试难度:单例依赖全局状态,可能增加单元测试复杂度(可通过依赖注入缓解)。
总结
单例模式通过控制实例唯一性,在资源管理和全局访问场景中发挥重要作用。选择实现方式时需权衡:
- 若需简单安全且可接受立即加载:饿汉式或枚举单例。
- 若需延迟加载且注重性能:双重检查锁(DCL)或静态内部类。
- 若需防反射和序列化:优先选择枚举单例