SpringBoot的单例模式是如何实现的?
SpringBoot的单例模式是如何实现的?
月伴飞鱼Spring Boot 里的单例不是 GoF 意义上的全 JVM 唯一对象 + 私有构造器。
而是由 Spring 容器管理的每个 ApplicationContext 仅一个实例。
其本质是 IOC 容器用缓存表保存已创建的 Bean,并在 getBean() 时复用。
默认就是单例
- 任何
@Component/@Service/@Repository/@Controller,或@Bean,默认 scope=singleton。 - 单例是容器级别:多个
ApplicationContext(测试、子上下文)各有一份。
1 | // 等价于 @Scope("singleton") |
容器如何保证只有一个
核心在 DefaultSingletonBeanFactory/DefaultSingletonBeanRegistry 的三级缓存与同步控制:
- 一级缓存
singletonObjects:已完全创建好的单例。 - 二级缓存
earlySingletonObjects:为解决循环依赖而“提前曝光”的半成品对象。 - 三级缓存
singletonFactories:可生成早期引用的工厂(通常是 AOP 代理工厂)。
创建流程(简化):
getBean(name)→ 若在singletonObjects里,直接返回。- 不在 → 标记“正在创建”,调用
createBean()完成实例化、依赖注入、BeanPostProcessor等。 - 初始化完成后放入
singletonObjects,清理二/三级缓存,返回单例。 - 全过程对同名 bean 的创建有同步锁,避免并发重复创建,并通过早期引用解决 A→B→A 的构造/注入级循环依赖。
何时创建
- 默认容器刷新时预创建(
preInstantiateSingletons())。 - 标注
@Lazy或全局开启spring.main.lazy-initialization=true时,延迟到首次getBean()才创建,但仍是单例。
线程安全与可见性
- 创建阶段受容器同步保护,发布到
singletonObjects前已完成依赖注入与后置处理,具备安全发布语义。 - 但单例是多线程共享:业务字段若是可变状态,需自行保证线程安全(不可变、同步、并发集合等)。
@Bean 与 @Configuration 的细节
@Configuration类会被 CGLIB 增强,其@Bean方法默认proxyBeanMethods=true。- 在同一配置类内部互相调用
@Bean方法时,调用会被拦截并从容器取同一个单例,不是 new 新对象。
- 在同一配置类内部互相调用
- 若显式
proxyBeanMethods=false(Spring Boot 为提速常用),同类内直接方法调用将变成普通方法调用,可能得到新对象。 - 只要是通过容器
getBean()拿的,仍是单例。
经验:配置类间若互调 @Bean 方法,保持默认 proxyBeanMethods=true 或避免直接互调。
常见误区
- Spring 单例 = JVM 唯一 ❌:是每个容器一个。
- 你起了两个
ApplicationContext就有两份。
- 你起了两个
- 用 static/单例写法再交给 Spring 管理 ❌:多此一举且易与容器生命周期冲突。
- 循环依赖全能解 ❌:仅能解单例构造器以外的注入型循环依赖,构造器循环、原型 scope、带代理的某些场景仍会失败。
最小验证
1 |
|
小结
Spring Boot 的单例= 默认 scope + BeanFactory 单例缓存 + 同步创建 +(必要时)早期曝光。
把唯一性交给容器,比手写 GoF 单例更易测、更可控,也能与 AOP、生命周期、条件化装配等机制无缝协作。











