Spring三级缓存:循环依赖的解决方案
在Spring框架中,循环依赖是Bean创建过程中常见的问题 针对这种问题 我们可以通过编码手段去解决
而Spring也为我们提供了一种解决手段 也就是三级缓存
本文将从缓存设计、解决逻辑、并发安全及场景限制等维度,详细解析三级缓存的工作原理。
一、三级缓存的核心设计目的
三级缓存是Spring为解决Bean循环依赖问题专门设计的缓存体系,其核心思路是通过“对象初始化延后”的方式,
在Bean未完全初始化时提前暴露引用,从而打破循环依赖的死锁。
二、三级缓存的定义与职责
Spring在DefaultSingletonBeanRegistry类中定义了三级缓存,各自承担不同职责:
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;
// 一级缓存:保存**完全初始化完成**的单例Bean(最终可用状态)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
// 二级缓存:保存**半成品Bean**(实例化完成、未初始化的早期引用)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
// 三级缓存:保存Bean的创建工厂(用于生成早期Bean引用)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
}
| 缓存层级 | 名称 | 核心作用 | 数据类型 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 存储完全初始化的成熟Bean | Map<String, Object> |
| 二级缓存 | earlySingletonObjects | 存储实例化完成的早期Bean引用 | Map<String, Object> |
| 三级缓存 | singletonFactories | 存储Bean的创建工厂 | Map<String, ObjectFactory> |
三、三级缓存解决循环依赖的核心逻辑
Spring将Bean的创建分为实例化(调用构造方法创建对象)和初始化(填充属性、执行初始化方法)两个阶段,三级缓存的核心作用是在“实例化后、初始化前”提前暴露Bean引用。
核心获取逻辑(getSingleton方法)
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 优先从一级缓存获取完全初始化的Bean
Object singletonObject = this.singletonObjects.get(beanName);
// 2. 一级缓存无数据,且当前Bean正在创建中 → 尝试二级缓存
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
// 3. 二级缓存无数据,且允许提前引用 → 从三级缓存生成早期Bean
if (singletonObject == null && allowEarlyReference) {
// 加锁保证缓存操作的线程安全(统一锁一级缓存避免多锁死锁)
synchronized (this.singletonObjects) {
// 双重检查:再次确认一级/二级缓存(防止并发写入)
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 4. 从三级缓存获取Bean工厂,生成早期Bean引用
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 5. 将早期Bean存入二级缓存,同时移除三级缓存的工厂
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
循环依赖解决流程(以A←→B循环依赖为例)
-
Spring创建Bean A,完成实例化后,将A的创建工厂存入三级缓存;
-
初始化A时,发现需要注入Bean B,触发B的创建流程;
-
Spring创建Bean B,完成实例化后,将B的创建工厂存入三级缓存;
-
初始化B时,发现需要注入Bean A,调用
getSingleton(A):- 一级缓存无A(未初始化完成),二级缓存无A;
- 从三级缓存获取A的工厂,生成A的早期引用,存入二级缓存;
- 将A的早期引用返回给B,B完成初始化,存入一级缓存;
-
A拿到B的成熟引用(一级缓存),完成自身初始化,存入一级缓存;
-
最终A、B均存入一级缓存,循环依赖问题解决。
四、缓存操作加锁的原因
缓存操作时对singletonObjects(一级缓存)加锁,核心目的是:
-
保证 缓存一致性:防止多线程并发操作缓存时,出现“读取到不完整数据”“重复创建Bean”等一致性问题;
-
避免 死锁 风险:统一锁定一级缓存,而非为三级缓存分别加锁,减少多锁嵌套导致的死锁概率;
-
双重检查保障:加锁后的“双重检查”逻辑,能有效防止并发场景下的缓存穿透。
那么 三级缓存一定能够解决循环依赖吗 答案是不能完全解决
在构造器注入产生的循环依赖 则需要采用其他方法
五、三级缓存的场景限制:无法解决构造器注入循环依赖
三级缓存仅能解决字段注入/setter注入的循环依赖,无法解决构造器注入的循环依赖,原因如下:
- Spring Bean的创建流程中,构造器注入发生在实例化阶段(调用构造方法时),此时Bean尚未完成实例化,无法生成早期引用;
- 三级缓存的核心逻辑是“实例化后、初始化前”提前暴露引用,而构造器注入的循环依赖发生在实例化阶段,缓存机制尚未介入。
构造器注入循环依赖的解决方案
通过Spring提供的@Lazy注解延迟构造器注入的Bean初始化:
@Lazy会让Spring在构造器注入时,返回一个Bean的代理对象(而非真实对象);- 真实对象的创建会延迟到首次使用时,从而打破构造器阶段的循环依赖。
示例:
@Component
public class A {
// 构造器注入B时添加@Lazy,延迟B的初始化
public A(@Lazy B b) {
this.b = b;
}
}
@Component
public class B {
public B(A a) {
this.a = a;
}
}
总结
- 三级缓存的核心是通过“实例化后提前暴露早期引用”,解决字段/setter注入的循环依赖;
- 缓存操作加锁是为了保证并发安全,统一锁一级缓存可避免死锁;
- 三级缓存无法解决构造器注入循环依赖,需通过
@Lazy注解延迟初始化解决。