单例模式 (Singleton Pattern)
模式定义
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
核心要点
- 私有构造函数,防止外部实例化
- 提供静态方法或字段获取唯一实例
- 线程安全的实现方式
实现方式
1. 饿汉式
类加载时就立即创建实例,线程安全,但无法延迟加载。
java
public class HungrySingleton {
// 类加载时就初始化
private static final HungrySingleton INSTANCE = new HungrySingleton();
// 私有构造函数
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return INSTANCE;
}
public void doSomething() {
System.out.println("HungrySingleton is working.");
}
}优点: 实现简单,线程安全。 缺点: 类加载时就创建,无论是否使用,浪费内存。
2. 双重检查锁 (DCL)
延迟加载 + 线程安全,是生产环境最常用的单例实现。
java
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;
}
public void doSomething() {
System.out.println("DCLSingleton is working.");
}
}为什么需要 volatile?instance = new DCLSingleton() 这行代码实际分为三步:
- 分配内存空间
- 初始化对象
- 将引用指向分配的内存
JVM 可能会对 2 和 3 进行重排序,导致其他线程看到未初始化的对象。volatile 禁止重排序。
3. 静态内部类
利用类加载机制实现延迟加载,同时保证线程安全。
java
public class InnerClassSingleton {
private InnerClassSingleton() {}
// 静态内部类,只有在被引用时才会加载
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void doSomething() {
System.out.println("InnerClassSingleton is working.");
}
}优点: 延迟加载 + 线程安全,无锁,性能好。 缺点: 无法传参初始化。
4. 枚举单例
《Effective Java》推荐的方式,天然防止反射和序列化攻击。
java
public enum EnumSingleton {
INSTANCE;
private int count;
public void doSomething() {
System.out.println("EnumSingleton is working. count=" + count);
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
// 使用方式
// EnumSingleton.INSTANCE.doSomething();优点: 简洁,天然线程安全,防止反射和序列化破坏。 缺点: 不支持延迟加载。
5. 序列化防御
防止反序列化创建新实例。
java
public class SerializableSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile SerializableSingleton instance;
private SerializableSingleton() {}
public static SerializableSingleton getInstance() {
if (instance == null) {
synchronized (SerializableSingleton.class) {
if (instance == null) {
instance = new SerializableSingleton();
}
}
}
return instance;
}
// 关键:反序列化时返回同一个实例
private Object readResolve() {
return instance;
}
public void doSomething() {
System.out.println("SerializableSingleton is working.");
}
}原理: Java 序列化机制在反序列化时,如果类定义了 readResolve() 方法,会调用该方法返回对象,从而保证单例。
6. 反射防御
防止通过反射创建新实例。
java
public class ReflectionSingleton {
private static volatile ReflectionSingleton instance;
private static boolean isCreated = false;
private ReflectionSingleton() {
// 防御反射攻击
if (isCreated) {
throw new RuntimeException("请使用 getInstance() 方法获取实例!");
}
isCreated = true;
}
public static ReflectionSingleton getInstance() {
if (instance == null) {
synchronized (ReflectionSingleton.class) {
if (instance == null) {
instance = new ReflectionSingleton();
}
}
}
return instance;
}
public void doSomething() {
System.out.println("ReflectionSingleton is working.");
}
}原理: 通过标志位判断是否已经创建过实例,如果是通过反射创建的,则抛出异常。
7. ThreadLocal 单例
保证每个线程有自己的单例实例(伪线程单例)。
java
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> THREAD_LOCAL =
ThreadLocal.withInitial(ThreadLocalSingleton::new);
private ThreadLocalSingleton() {}
public static ThreadLocalSingleton getInstance() {
return THREAD_LOCAL.get();
}
public void doSomething() {
System.out.println("ThreadLocalSingleton is working in thread: "
+ Thread.currentThread().getName());
}
}特点: 每个线程获取到的是不同的实例,适用于线程隔离场景。
各实现方式对比
| 实现方式 | 线程安全 | 延迟加载 | 防反射 | 防序列化 | 推荐场景 |
|---|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | ❌ | ❌ | 实例占用资源少 |
| DCL | ✅ | ✅ | ❌ | ❌ | 通用场景 |
| 静态内部类 | ✅ | ✅ | ❌ | ❌ | 通用场景,性能要求高 |
| 枚举 | ✅ | ❌ | ✅ | ✅ | 强烈推荐 |
| 序列化防御 | ✅ | ✅ | ❌ | ✅ | 需要序列化的场景 |
| 反射防御 | ✅ | ✅ | ✅ | ❌ | 安全性要求高 |
| ThreadLocal | 线程内 | ✅ | ❌ | ❌ | 线程隔离场景 |
实际应用
- Spring Bean: 默认 scope=singleton,容器中只有一个 Bean 实例
- 数据库连接池: 保证全局唯一连接池
- 配置管理器: 全局配置信息
- 日志记录器: 全局日志实例
- Runtime 类:
Runtime.getRuntime()就是饿汉式单例
注意事项
- 线程安全 是单例实现的首要考虑因素
- DCL 中
volatile关键字不可省略 - 枚举 是最安全的单例实现方式
- 多线程环境下,避免使用简单同步(性能差)
- 序列化和反射是单例的两大"杀手",需要额外防御