Spring 的单例实现原理
Spring 对 Bean 实例的创建是采用单例注册表的方式进行实现的,而这个注册表的缓存是 ConcurrentHashMap 对象。
这句话只说对了三分之一:缓存确实是并发 Map,但 Spring 单例的真正复杂度不在”存”,而在”创建过程如何保证只创建一次、且能处理循环依赖“。本文按单例语义、三级缓存、创建流程三层展开。
Spring 单例 ≠ GoF 单例
设计模式的单例是”一个类在全 JVM 只有一个实例”(私有构造 + 静态持有);Spring 的单例是”一个 beanName 在容器单例注册表中只有一个实例“(singleton scope)。同一个类可以定义两个 bean(不同 name)各持一个实例,也可以被 prototype scope 反复新建。所以 Spring 单例是容器级的约定,靠注册表约束而非语言机制。
单例注册表:三级缓存
DefaultSingletonBeanRegistry 中维护三个 Map:
singletonObjects(一级):完成品缓存,存放完全初始化好的 beanearlySingletonObjects(二级):半成品缓存,存放提前暴露的早期引用(可能是代理对象)singletonFactories(三级):ObjectFactory 缓存,lambda 形式,需要时才生成早期引用(决定是否套代理)
为什么需要三级而不是一级?核心矛盾是循环依赖:A 依赖 B,B 又依赖 A。创建 A 时需要注入 B → 创建 B → B 注入 A 时 A 还没创建完。解法是”提前暴露”:A 实例化(构造器完成、属性未填)后,立刻把一个能返回自身早期引用的工厂放进三级缓存;B 通过它拿到 A 的早期引用完成注入,A 再继续填自己的属性。
三级存在的精细之处:若 A 需要被代理,早期引用必须是代理对象而非原始对象。ObjectFactory 延迟到”有人来要早期引用”时才决定返回原始还是代理,避免所有 bean 都提前创建代理的浪费。二级则保证同一早期引用只被提升一次、多处拿到的是同一个对象。
getBean 主流程
- 查一级缓存,命中直接返回
- 未命中且该 bean 正在创建中(singletonsCurrentlyInCreation)→ 查二级 → 查三级并调用工厂、结果升入二级
- 都没有则开始创建:
createBeanInstance(构造器/工厂方法实例化)→ 放入三级缓存(addSingletonFactory)→populateBean(依赖注入,可能递归触发上面的流程)→initializeBean(Aware 回调、初始化方法、AOP 代理)→ 放入一级、清理二三级
beforeSingletonCreation / afterSingletonCreation 配对标记创建状态,配合 synchronized 保证同一 bean 的创建过程串行——并发 getBean 同一 bean 时,只有一个线程真正创建,其余等待并复用结果。
边界与坑
- 构造器循环依赖无解:三级缓存只能暴露”已实例化”的早期引用,构造器注入时实例都不存在,直接抛 BeanCurrentlyInCreationException。解法:改 setter/字段注入、@Lazy 打断
- prototype 不参与缓存:每次 getBean 新建,且 prototype 之间的循环依赖容器不管
- 单例 bean 持有 prototype bean:注入只发生一次,prototype 实际退化为单例使用;需要每次新实例应注入 ObjectProvider 或 ApplicationContext 手动 get
- 单例 bean 的有状态字段是线程安全雷区:容器只保证实例唯一,不保证线程安全,Service 里放可变成员变量等于共享状态
Spring 单例机制的精华是三级缓存对”创建中对象”的精细管理:它把”实例化”与”初始化”拆开,在两者之间插入一个可被安全引用的窗口,才让循环依赖与 AOP 代理得以共存。理解这个窗口,就理解了 Spring 容器最核心的一段设计。











