单例模式(Singleton Pattern):确保一个类仅有一个实例
单例模式是 创建型设计模式 的核心之一,核心目标是:一个类在整个应用生命周期中,只允许创建一个实例对象,并提供全局唯一的访问入口。
它广泛应用于需要共享资源、避免重复创建开销的场景(如配置管理、数据库连接池、日志工具、Spring 的默认 Bean 等)。
一、单例模式的核心原则
- 唯一实例 :类的构造器必须私有化(
private),禁止外部通过new关键字创建实例; - 全局访问 :提供公开的静态方法(如
getInstance()),作为获取唯一实例的唯一入口; - 线程安全 :多线程环境下,需避免并发调用
getInstance()时创建多个实例; - 懒加载 / 饿加载:根据实例创建时机,分为 "需要时才创建"(懒加载)和 "类加载时就创建"(饿加载)。
二、单例模式的常见实现方式(Java)
1. 饿汉式(Eager Initialization):类加载时创建实例
核心思路:
类加载阶段(JVM 加载类时)就初始化唯一实例,依赖 JVM 的类加载机制保证线程安全(JVM 对类加载是 "单线程" 的,不会并发初始化)。
实现代码:
java
运行
public class EagerSingleton {
// 1. 私有静态实例(类加载时直接初始化,饿汉="饿了马上吃")
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 2. 私有化构造器:禁止外部new
private EagerSingleton() {}
// 3. 公开静态方法:返回唯一实例
public static EagerSingleton getInstance() {
return INSTANCE;
}
// 示例:单例的业务方法
public void doSomething() {
System.out.println("饿汉式单例:" + this.hashCode());
}
}
优缺点:
- 优点:实现简单、天然线程安全(依赖 JVM 类加载机制)、无锁开销,获取实例速度快;
- 缺点:懒加载失效(类加载时就创建实例),如果实例从未被使用,会造成内存浪费(如重量级对象:数据库连接池)。
适用场景:
实例占用内存小、确定会被使用,或需要提前初始化的场景(如系统核心配置)。
2. 懒汉式(Lazy Initialization):需要时才创建实例
核心思路:
类加载时不创建实例,第一次调用 getInstance() 时才初始化,实现 "懒加载"(节省内存)。注意:默认非线程安全,需手动处理并发问题。
(1)基础懒汉式(非线程安全,禁止使用)
java
运行
public class LazySingletonUnsafe {
// 1. 私有静态实例(仅声明,不初始化)
private static LazySingletonUnsafe instance;
// 2. 私有化构造器
private LazySingletonUnsafe() {}
// 3. 公开静态方法:第一次调用时创建实例
public static LazySingletonUnsafe getInstance() {
if (instance == null) { // 多线程下,多个线程可能同时进入这里
instance = new LazySingletonUnsafe(); // 导致创建多个实例
}
return instance;
}
}
- 问题 :多线程并发调用
getInstance()时,多个线程会同时通过instance == null判断,进而创建多个实例,破坏单例原则。
(2)线程安全的懒汉式(加锁 synchronized,低效)
java
运行
public class LazySingletonSynchronized {
private static LazySingletonSynchronized instance;
private LazySingletonSynchronized() {}
// 给整个方法加锁:保证同一时间只有一个线程能进入
public static synchronized LazySingletonSynchronized getInstance() {
if (instance == null) {
instance = new LazySingletonSynchronized();
}
return instance;
}
}
- 优点:解决线程安全问题;
- 缺点:锁粒度太大(整个方法加锁),即使实例已创建,后续所有线程获取实例时仍需排队等待,并发效率极低。
(3)双重检查锁定(Double-Check Locking,DCL):高效线程安全
核心优化:
- 第一次检查
instance == null:避免实例已创建时的锁竞争; - 第二次检查
instance == null:防止多个线程同时通过第一次检查后,重复创建实例; volatile关键字:禁止指令重排序(关键!避免 "半初始化实例" 问题)。
实现代码(最终版):
java
运行
public class LazySingletonDCL {
// 1. 私有静态实例 + volatile 修饰(禁止指令重排序)
private static volatile LazySingletonDCL instance;
// 2. 私有化构造器
private LazySingletonDCL() {}
// 3. 双重检查锁定:高效线程安全
public static LazySingletonDCL getInstance() {
// 第一次检查:实例已存在则直接返回,避免锁竞争
if (instance == null) {
synchronized (LazySingletonDCL.class) { // 类锁:锁定整个类
// 第二次检查:防止多个线程同时进入第一次检查后,重复创建
if (instance == null) {
instance = new LazySingletonDCL();
// 注意:new操作非原子性,volatile避免指令重排序导致的"半初始化"
}
}
}
return instance;
}
public void doSomething() {
System.out.println("DCL懒汉式单例:" + this.hashCode());
}
}
关键说明:volatile 的作用
new LazySingletonDCL() 实际分为 3 步:
- 分配内存空间;
- 初始化实例(调用构造器);
- 将实例引用指向内存空间。
JVM 可能会对指令重排序(如 1→3→2),导致线程 A 执行到 3 时,线程 B 看到 instance != null,但实例未完成初始化(半初始化状态),进而报错。volatile 关键字会禁止指令重排序,保证 1→2→3 的执行顺序,避免该问题。
优缺点:
- 优点:懒加载(节省内存)、线程安全、并发效率高(仅初始化时加锁,后续无锁);
- 缺点 :实现稍复杂,需理解
volatile和双重检查的逻辑。
适用场景:
实例占用内存大、不确定是否会被使用,且需要高并发访问的场景(推荐首选)。
3. 静态内部类(Static Inner Class):完美结合懒加载与线程安全
核心思路:
利用 Java 的 "静态内部类加载机制":静态内部类不会随外部类加载而初始化,只有第一次调用其静态成员时才会加载,且加载过程线程安全。
实现代码:
java
运行
public class StaticInnerClassSingleton {
// 1. 私有静态内部类:存储唯一实例
private static class SingletonHolder {
// 内部类加载时初始化实例(线程安全)
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
// 2. 私有化构造器
private StaticInnerClassSingleton() {}
// 3. 公开静态方法:触发内部类加载,返回实例
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void doSomething() {
System.out.println("静态内部类单例:" + this.hashCode());
}
}
原理:
- 外部类
StaticInnerClassSingleton加载时,内部类SingletonHolder不会加载,实现懒加载; - 第一次调用
getInstance()时,触发SingletonHolder加载,初始化INSTANCE,JVM 保证该过程线程安全; - 后续调用直接返回已初始化的实例,无锁开销。
优缺点:
- 优点:懒加载、线程安全、实现简单、无锁高效(集合了饿汉式和 DCL 的优点);
- 缺点:无法传参初始化(如果实例需要依赖外部参数,该方式不适用)。
适用场景:
无需传参初始化的单例(推荐首选,代码简洁且无潜在问题)。
4. 枚举单例(Enum Singleton):终极解决方案
核心思路:
利用 Java 枚举(Enum)的天然特性:
- 枚举类的实例是天然单例(JVM 保证);
- 枚举类的构造器默认是私有(无法通过
new创建); - 支持序列化 / 反序列化安全(避免反射破坏单例)。
实现代码:
java
运行
public enum EnumSingleton {
// 唯一实例(枚举常量即为单例实例)
INSTANCE;
// 单例的业务方法(直接在枚举中定义)
public void doSomething() {
System.out.println("枚举单例:" + this.hashCode());
}
// (可选)如果需要复杂初始化,可添加构造器
EnumSingleton() {
// 初始化逻辑(如加载配置、连接资源)
System.out.println("枚举单例初始化");
}
}
使用方式:
java
运行
// 直接通过枚举常量获取实例
EnumSingleton singleton = EnumSingleton.INSTANCE;
singleton.doSomething();
核心优势(为什么是 "终极方案"):
- 天然线程安全:JVM 保证枚举实例的初始化是单线程的,无需手动加锁;
- 防止反射破坏 :Java 反射机制无法创建枚举类的实例(会抛出
IllegalArgumentException); - 序列化安全 :普通单例序列化后反序列化会创建新实例,枚举类默认重写了
readResolve()方法,保证反序列化后仍是原实例; - 实现极简:一行代码定义实例,无需处理懒加载、线程安全等细节。
缺点:
- 懒加载失效(枚举类加载时就初始化实例,与饿汉式类似);
- 枚举实例默认是单例,无法动态创建多个实例(符合单例原则,非真正缺点)。
适用场景:
需要绝对安全(防止反射、序列化破坏)的单例场景(如核心配置、安全组件),是《Effective Java》作者推荐的最优单例实现。
三、单例模式的常见问题与解决方案
1. 反射破坏单例
问题:
普通单例(如饿汉式、DCL)的私有构造器,可通过 Java 反射机制强制调用,创建新实例:
java
运行
// 反射破坏饿汉式单例
Class<?> clazz = EagerSingleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true); // 跳过访问检查
EagerSingleton instance1 = EagerSingleton.getInstance();
EagerSingleton instance2 = (EagerSingleton) constructor.newInstance();
System.out.println(instance1 == instance2); // false(破坏单例)
解决方案:
-
方案 1:使用枚举单例(天然防止反射);
-
方案 2:在私有构造器中添加校验,禁止重复创建: java
运行
private EagerSingleton() { // 防止反射破坏:如果实例已存在,抛出异常 if (INSTANCE != null) { throw new IllegalStateException("单例实例已存在,禁止重复创建"); } }
2. 序列化 / 反序列化破坏单例
问题:
普通单例实现 Serializable 接口后,序列化后再反序列化会创建新实例:
java
运行
// 序列化普通单例
EagerSingleton instance1 = EagerSingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
oos.writeObject(instance1);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
EagerSingleton instance2 = (EagerSingleton) ois.readObject();
System.out.println(instance1 == instance2); // false(破坏单例)
解决方案:
-
方案 1:使用枚举单例(天然支持序列化安全);
-
方案 2:在普通单例中重写
readResolve()方法,返回已有的单例实例:java
运行
public class EagerSingleton implements Serializable { private static final EagerSingleton INSTANCE = new EagerSingleton(); private EagerSingleton() {} public static EagerSingleton getInstance() { return INSTANCE; } // 反序列化时调用,返回已有实例 private Object readResolve() { return INSTANCE; } }
3. 多 ClassLoader 环境下的单例问题
问题:
如果应用中有多个类加载器(如 Tomcat 的 WebAppClassLoader),每个类加载器会单独加载单例类,导致每个类加载器下都有一个单例实例(破坏 "全局唯一")。
解决方案:
- 让单例类由 "父类加载器" 加载(如 JVM 的 Bootstrap ClassLoader);
- 在
getInstance()中指定类加载器,确保全局使用同一个类加载器。
四、单例模式的应用场景
- 资源共享类 :如日志工具(
Logger)、配置管理器(Config)、线程池、数据库连接池(DataSource); - 重量级对象:创建成本高的对象(如网络连接、大型缓存),避免重复创建开销;
- 全局状态管理:需要统一维护状态的对象(如计数器、全局上下文);
- 框架默认实现 :Spring 容器中的 Bean 默认是单例(
singleton作用域),通过 JavaConfig 或 XML 配置管理。
五、单例模式的优缺点总结
优点:
- 减少内存开销:仅创建一个实例,避免重复创建重量级对象;
- 统一访问控制:全局唯一入口,便于维护对象状态和资源共享;
- 避免资源冲突:如数据库连接池避免多线程并发创建过多连接导致的资源耗尽。
缺点:
- 违背单一职责原则:单例类既要负责自身业务逻辑,又要负责实例创建和管理;
- 扩展性差:单例类通常没有接口,无法通过继承扩展(除非修改原代码);
- 测试困难:单例类依赖全局状态,难以进行单元测试(无法模拟不同实例的场景);
- 可能引发内存泄漏:单例实例生命周期与应用一致,若持有外部对象引用且未释放,会导致内存泄漏。
六、常见实现方式对比表
| 实现方式 | 懒加载 | 线程安全 | 防止反射 | 序列化安全 | 实现复杂度 | 推荐度 |
|---|---|---|---|---|---|---|
| 饿汉式 | ❌ | ✅ | ❌ | ❌ | 简单 | ⭐⭐⭐ |
| 基础懒汉式(非线程安全) | ✅ | ❌ | ❌ | ❌ | 简单 | ❌ |
| 同步方法懒汉式 | ✅ | ✅ | ❌ | ❌ | 简单 | ⭐⭐ |
| 双重检查锁定(DCL) | ✅ | ✅ | ❌ | ❌ | 中等 | ⭐⭐⭐⭐ |
| 静态内部类 | ✅ | ✅ | ❌ | ❌ | 简单 | ⭐⭐⭐⭐⭐ |
| 枚举单例 | ❌ | ✅ | ✅ | ✅ | 极简 | ⭐⭐⭐⭐⭐ |
总结
- 若需 懒加载 + 无复杂需求:首选「静态内部类」(简单高效);
- 若需 绝对安全(防反射 / 序列化):首选「枚举单例」(终极方案);
- 若需 传参初始化:选择「双重检查锁定(DCL)」;
- 若实例 占用内存小、确定会使用:选择「饿汉式」(简单直接);
- 避免使用「基础懒汉式」和「同步方法懒汉式」(前者线程不安全,后者并发低效)。
单例模式是最常用的设计模式之一,但需注意避免滥用(如无需全局唯一的场景强行使用,会导致扩展性和测试性问题)。