单例模式 (Singleton Pattern)
什么是单例模式?
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
适用场景
- 配置管理:应用程序的配置信息只需要加载一次。
- 日志记录器:整个系统共享一个日志实例。
- 数据库连接池:避免频繁创建和销毁连接。
- 线程池:统一管理线程资源。
实现方式
1. 饿汉式(Eager Initialization)
类加载时就创建实例,线程安全但不支持延迟加载。
java
/**
* 饿汉式单例
* @author yjhu
*/
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}2. 懒汉式(Lazy Initialization)
需要时才创建实例,但需要处理线程安全问题。
java
/**
* 懒汉式单例(双重检查锁)
* @author yjhu
*/
public class LazySingleton {
// volatile 防止指令重排序
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (LazySingleton.class) {
if (instance == null) { // 第二次检查
instance = new LazySingleton();
}
}
}
return instance;
}
}为什么需要双重检查?
| 检查位置 | 作用 |
|---|---|
| 第一次检查(synchronized 外) | 避免每次调用都进入同步块,提升性能。如果实例已创建,直接返回。 |
| 第二次检查(synchronized 内) | 防止多个线程同时通过第一次检查后,重复创建实例。 |
场景模拟:假设线程 A 和线程 B 同时调用 getInstance():
- 两者都通过第一次
if (instance == null)检查 - 线程 A 先获取锁,创建实例,释放锁
- 线程 B 获取锁后,如果没有第二次检查,会再次创建实例 ❌
为什么需要 volatile?
instance = new LazySingleton() 这行代码并非原子操作,实际分为三步:
1. 分配内存空间
2. 初始化对象
3. 将 instance 指向分配的内存地址由于 JVM 的指令重排序优化,实际执行顺序可能变成 1 → 3 → 2。
问题场景:
- 线程 A 执行到步骤 3(
instance已指向内存,但对象未初始化) - 线程 B 进入
getInstance(),第一次检查发现instance != null - 线程 B 直接返回未初始化完成的对象 ❌
volatile 的作用:
- 禁止指令重排序,确保
1 → 2 → 3的执行顺序 - 保证可见性,一个线程修改后,其他线程立即可见
3. 静态内部类(推荐)
兼顾线程安全和延迟加载,是 Java 中最优雅的实现方式。
java
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* 静态内部类单例模式
* @author yjhu
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Singleton {
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}原理:JVM 在加载外部类时不会加载静态内部类,只有在调用 getInstance() 时才会加载 Holder 类,从而实现延迟加载。同时,类加载过程是线程安全的。
4. 枚举方式(最安全)
《Effective Java》推荐的方式,天然防止反射攻击和序列化问题。
java
/**
* 枚举单例
* @author yjhu
*/
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// 业务逻辑
}
}各实现方式对比
| 实现方式 | 线程安全 | 延迟加载 | 防反射攻击 | 防序列化攻击 |
|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | ❌ | ❌ |
| 双重检查锁 | ✅ | ✅ | ❌ | ❌ |
| 静态内部类 | ✅ | ✅ | ❌ | ❌ |
| 枚举 | ✅ | ❌ | ✅ | ✅ |
注意事项
- 反射攻击:除枚举外,其他方式都可以通过反射调用私有构造函数创建新实例。
- 序列化问题:如果单例类实现了
Serializable,反序列化时会创建新对象,需要添加readResolve()方法。 - Spring 中的单例:Spring 的
@Scope("singleton")是容器级别的单例,与设计模式中的单例不同。
