单利模式
核心目标非常明确:保证一个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取它
核心原则:没有最好的模式,只有最适合场景的模式
一、为什么需要单例?
在以下场景中,一个类只需要一个实例:
- 配置管理:整个系统共享一份配置信息(如数据库连接配置)。
- 日志记录:所有模块都往同一个日志文件中写入。
- 连接池:管理数据库连接,所有线程共享一个连接池。
- Spring容器中的Bean:默认就是单例的(
@Scope("singleton"))。
如果没有单例模式,多个地方 new出多个实例,会导致资源浪费和数据不一致。
二、单例模式的5种实现方式(Java)
单例模式的实现核心是:
- 私有化构造器,防止外部
new。 - 提供一个静态方法,返回唯一的实例。
常见的实现方式有5种,我按推荐度从低到高排列:
方式1:饿汉式(线程安全,最简单)
public class Singleton {
// 1. 类加载时就创建实例(静态变量)
private static final Singleton INSTANCE = new Singleton();
// 2. 私有化构造器
private Singleton() {}
// 3. 提供全局访问点
public static Singleton getInstance() {
return INSTANCE;
}
}
特点:
- ✅ 线程安全:由JVM类加载机制保证。
- ✅ 简单:代码最少。
- ❌ 立即加载:即使从未使用,类加载时也会创建,浪费内存(如果实例很重,如大对象)。
方式2:懒汉式(线程不安全,不推荐)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // ❌ 多线程下会创建多个实例
instance = new Singleton();
}
return instance;
}
}
特点:
- ✅ 懒加载:需要时才创建。
- ❌ 线程不安全:多个线程同时调用
getInstance()可能创建多个实例。
方式3:synchronized懒汉式(线程安全,但效率低)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
特点:
- ✅ 线程安全:方法加锁。
- ✅ 懒加载。
- ❌ 效率低:每次调用都要获取锁,只有第一次创建时需要同步,后续应该直接返回实例。
方式4:双重检查锁(DCL,推荐)
这是实际开发中最常用的方式:
public class Singleton {
// 1. volatile 防止指令重排序
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 2. 第一次检查:避免不必要的同步
if (instance == null) {
// 3. 同步块:只锁定创建实例的代码
synchronized (Singleton.class) {
// 4. 第二次检查:防止多线程重复创建
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
为什么需要 volatile?
new Singleton()在JVM中不是原子操作,可能发生指令重排序,导致其他线程拿到未初始化完全的对象。volatile禁止了重排序,保证了可见性。
特点:
- ✅ 线程安全。
- ✅ 懒加载。
- ✅ 效率较高:只在第一次创建时同步。
- ⚠️ 较复杂:需要理解
volatile和DCL的原理。
方式5:静态内部类(最优方案,强烈推荐)
利用JVM的类加载机制,既实现懒加载,又保证线程安全:
public class Singleton {
// 私有构造器
private Singleton() {}
// 静态内部类(只有在调用 getInstance() 时才会加载)
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
特点:
- ✅ 线程安全:由JVM类加载机制保证。
- ✅ 懒加载:
Holder类只在getInstance()被调用时才加载,从而创建实例。 - ✅ 代码简洁:比DCL更清晰。
- ✅ 无性能损耗:没有同步锁。
三、反射和序列化攻击怎么办?
即使私有化构造器,反射仍可以强行调用构造器,破坏单例。
防御方式1:在构造器中检测并抛异常
public class Singleton {
private static boolean isCreated = false;
private Singleton() {
if (isCreated) {
throw new RuntimeException("单例模式禁止反射创建");
}
isCreated = true;
}
}
防御方式2:使用 枚举(最安全)
枚举类型由JVM保证只会被实例化一次,即使反射也无法破坏:
public enum Singleton {
INSTANCE; // 单例对象
// 可以添加方法
public void doSomething() {
System.out.println("Do something...");
}
}
// 使用
Singleton.INSTANCE.doSomething();
为什么枚举最安全?
- 枚举的构造器是JVM保护的,反射无法调用。
- 序列化机制对枚举有特殊处理,不会创建新实例。
四、实际应用
1.Runtime.getRuntime()
// Java标准库源码(简化版)
public class Runtime {
// 1. 饿汉式:类加载时即创建唯一实例
private static final Runtime currentRuntime = new Runtime();
// 2. 私有化构造器,禁止外部创建
private Runtime() {}
// 3. 全局访问点
public static Runtime getRuntime() {
return currentRuntime;
}
// 业务方法:获取处理器核心数
public int availableProcessors() {
return nativeAvailableProcessors();
}
// 业务方法:执行系统命令
public Process exec(String command) throws IOException {
return exec(command, null, null);
}
}
为什么用饿汉式
Runtime 封装的是 JVM 的运行时环境。只要 JVM 启动,运行时环境就一定存在,这是毋庸置疑的。它不像某些业务工具类(如 DateFormat)可能永远用不到,需要延迟加载来节省资源。
既然无论如何都需要创建,那“立即加载”就不是缺点,反而是最符合逻辑的做法
2.Spring框架中的容器化单例:单例池(Singleton Pool)
Spring的单例不同于传统的“ClassLoader全局唯一”,它是“容器内唯一”。每个Spring容器(ApplicationContext)维护一个单例池(Singleton Pool),通过 DefaultSingletonBeanRegistry 的核心缓存来管理。
核心数据结构:一级缓存
// DefaultSingletonBeanRegistry 源码(简化)
public class DefaultSingletonBeanRegistry {
// 单例池:存放所有完全初始化好的单例Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 注册单例Bean
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject);
// 从其他缓存中移除...
}
}
// 获取单例Bean
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
return this.singletonObjects.get(beanName);
}
}
为什么Spring要设计自己的单例池
依赖注入(DI)、生命周期管理等复杂场景 DCL等模式不适合,并不是为了单例而单例
五、三级缓存
1. 核心数据结构:三级缓存机制
DefaultSingletonBeanRegistry 内部维护了三个缓存,这是 Spring 单例管理的基石:
// 一级缓存:存放完全初始化好的单例 Bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存放实例化完成但尚未填充属性的早期 Bean(半成品)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:存放 Bean 的工厂对象,用于生成早期引用
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
这三层缓存的设计,一方面保证了Bean获取的高效,另一方面也是Spring解决循环依赖问题的关键所在。
2. 核心流程:获取与创建
当你通过 getBean("myService") 获取一个单例Bean时,Spring的处理流程大致如下:
-
尝试获取:首先会调用
getSingleton(beanName)方法,从上述三级缓存中尝试查找已创建好的实例。- 如果在
singletonObjects中找到了,直接返回成品。 - 如果没找到,但该Bean正在创建中,可能会从
earlySingletonObjects或singletonFactories中获取早期引用(用于解决循环依赖)。
- 如果在
-
实例化与创建:如果缓存中不存在,Spring会进入创建流程,核心在
createBean()和doCreateBean()方法中。- 实例化:通过反射调用构造方法创建Bean实例(此时是半成品,属性还未注入)。
- 提前暴露:将这个半成品的工厂对象存入三级缓存
singletonFactories,以便应对循环依赖。 - 填充属性:执行
populateBean(),完成依赖注入。 - 初始化:执行
initializeBean(),调用@PostConstruct等初始化方法。 - 存入缓存:最终,将完全初始化好的Bean放入一级缓存
singletonObjects,并从其他缓存中移除。
3. 关键突破:循环依赖的解法
这是 Spring 单例管理的精髓。当A依赖B,B又依赖A时,Spring通过三级缓存巧妙化解:
- 实例化 A,将 A 的工厂对象放入三级缓存。
- 填充 A 的属性时,发现需要 B。
- 实例化 B,将 B 的工厂对象放入三级缓存。
- 填充 B 的属性时,发现需要 A。
- 此时,Spring 从三级缓存中获取 A 的工厂对象,生成一个A的早期引用(半成品),并将其提升到二级缓存。
- B 成功注入这个 A 的早期引用,完成初始化,B 成为成品进入一级缓存。
- A 继续完成初始化,最终也进入一级缓存。
注意:这种解决方式仅限于单例Bean的setter注入。构造器注入的循环依赖或原型Bean的循环依赖,Spring无法处理。在Spring Boot 2.6.x之后,循环依赖的默认行为也变成了“禁止”。
4. 核心区别一览
| 特性 | Java 单例模式 | Spring 单例 Bean |
|---|---|---|
| 管理者 | 类自身(通过静态变量和私有构造器) | IoC 容器(通过 DefaultSingletonBeanRegistry) |
| 唯一性范围 | 全局唯一(同一个 ClassLoader) | 容器内唯一(不同容器可有不同实例) |
| 生命周期控制 | 静态,由 JVM 控制 | 由容器全权管理,支持初始化和销毁回调 |
| 依赖注入 | 自身难以管理复杂依赖(通常需要组合其他单例) | 原生支持,通过 @Autowired 等注解无缝注入 |
| 扩展能力 | 较弱,通常通过继承或接口扩展 | 极强,支持 AOP、代理、作用域切换(@Scope)等 |
简单来说,Java单例模式是一种“创造模式”,而Spring的单例Bean则是一种“管理服务”。后者在保证唯一性的基础上,解决了依赖注入、生命周期等更复杂的工程问题。