跳到主要内容

Spring 循环依赖问题(三级缓存)

Spring 是如何解决循环依赖的问题的。

何为循环依赖​

就是这样:A、B、C 之间相互依赖

造成的问题​

来一串代码说明问题

public class A {
private B b;
}

public class B {
private A a;
}

对应的 Bean 的配置

<bean id="beanA" class="xyz.coolblog.BeanA">
<property name="beanB" ref="beanB"/>
</bean>

<bean id="beanB" class="xyz.coolblog.BeanB">
<property name="beanA" ref="beanA"/>
</bean>

IOC 按照上面所示的 <bean> 配置,实例化 A 的时候发现 A 依赖于 B 于是去实例化 B(此时 A 创建未结束,处于创建中的状态),而发现 B 又依赖于 A ,于是就这样循环下去,最终导致 OOM

循环依赖发生的时机​

Bean 实例化主要分为三步,如图:

问题出现在:第一步和第二步的过程中,也就是填充属性 / 方法的过程中(如果是使用构造方法注入就是第一步)

Spring 如何解决的(三级缓存)​

Spring 为了解决单例的循环依赖问题,使用了 三级缓存 ,递归调用时发现 Bean 还在创建中即为循环依赖

单例模式的 Bean 保存在如下的数据结构中:

/** 一级缓存:用于存放完全初始化好的 bean **/
private final Map<String, Object> singletonObjects =
new ConcurrentHashMap<>(256);

/** 二级缓存:存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 */
private final Map<String, Object> earlySingletonObjects =
new HashMap<>(16);

/** 三级级缓存:存放 bean 工厂对象,用于解决循环依赖 */
private final Map<String, ObjectFactory<?>> singletonFactories =
new HashMap<>(16);

bean 的获取过程:先从一级获取,失败再从二级、三级里面获取

创建中状态:是指对象已经 new 出来了但是所有的属性均为 null 等待被 init

检测循环依赖的过程如下:

1、A 创建过程中需要 B,于是 A 将自己放到三级缓里面 ,去实例化 B

2、B 实例化的时候发现需要 A,于是 B 先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了!

  • 然后把三级缓存里面的这个 A 放到二级缓存里面,并删除三级缓存里面的 A
  • B 顺利初始化完毕,将自己放到一级缓存里面(此时 B 里面的 A 依然是创建中状态)

3、然后回来接着创建 A,此时 B 已经创建结束,直接从一级缓存里面拿到 B ,然后完成创建,并将自己放到一级缓存里面

如此一来便解决了循环依赖的问题

// 以上叙述的源代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 先从一级缓存找
Object singletonObject = this.singletonObjects.get(beanName);
// 设置当前这个 beanName 为创建状态
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 再从二级缓存里面找
singletonObject = this.earlySingletonObjects.get(beanName);

if (singletonObject == null && allowEarlyReference) {
// 还找不到则使用三级缓存
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);

if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 把这个 beanName 存入二级缓存里面
this.earlySingletonObjects.put(beanName, singletonObject);
// 把当前这个 beanName 从三级缓存中删除
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

二级缓存​

我们现在已经知道,第三级缓存的目的是为了延迟代理对象的创建,因为如果没有依赖循环的话,那么就不需要为其提前创建代理,可以将它延迟到初始化完成之后再创建。

既然目的只是延迟的话,那么我们是不是可以不延迟创建,而是在实例化完成之后,就为其创建代理对象,这样我们就不需要第三级缓存了。因此,我们可以将 addSingletonFactory() 方法进行改造。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");

synchronized (this.singletonObjects) {
// 判断一级缓存中不存在此对象
if (!this.singletonObjects.containsKey(beanName)) {
// 直接从工厂中获取 Bean
Object o = singletonFactory.getObject();

// 添加至二级缓存中
this.earlySingletonObjects.put(beanName, o);
this.registeredSingletons.add(beanName);
}
}
}

这样的话,每次实例化完 Bean 之后就直接去创建代理对象,并添加到二级缓存中。

测试结果是完全正常的,Spring 的初始化时间应该也是不会有太大的影响,因为如果 Bean 本身不需要代理的话,是直接返回原始 Bean 的,并不需要走复杂的创建代理 Bean 的流程。

两级不能解决么?​

测试证明,二级缓存也是可以解决循环依赖的。为什么 Spring 不选择二级缓存,而要额外多添加一层缓存呢?

但是解决循环依赖真的需要使用到三级缓冲吗?只使用两级缓存是否可以呢?

回顾一下三级缓存如何解决的?

如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。(就是无法在初始化完对象后执行自定义的 init 方法了,具体看 Spring Bean 生命周期常见的拓展点 这篇笔记)

所以,Spring 选择了三级缓存。但是因为循环依赖的出现,导致了 Spring 不得不提前去创建代理,因为如果不提前创建代理对象,那么注入的就是原始对象,这样就会产生错误。(即无法初始化)

Reference​

参考资料 一文说透 Spring 循环依赖问题 参考资料 高频面试题:Spring 如何解决循环依赖? 参考资料 Spring 解决循环依赖必须要三级缓存吗?