循环依赖
循环依赖指的是两个或多个 Bean 相互依赖,形成一个闭环。这样的依赖关系会导致 Spring 容器无法正确创建和注入这些 Bean。
循环依赖会带来以下问题:
Bean 创建失败:Spring 容器在实例化一个 Bean 时,发现它依赖于另一个尚未创建的 Bean。如果两个 Bean 形成了循环依赖,Spring 就无法决定从哪个 Bean 开始创建,从而导致无法完成 Bean 的实例化和依赖注入。
内存泄漏风险:如果循环依赖没有得到解决,可能会导致一些 Bean 被创建但是无法被销毁,从而增加内存的占用,造成内存泄漏。
情景
假设有两个 Bean,A 和 B,它们之间有 构造器注入 关系,并且互相依赖:
@Component
public class A {
private final B b;
@Autowired
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private final A a;
@Autowired
public B(A a) {
this.a = a;
}
}
循环依赖死锁的过程
A Bean 初始化开始:Spring 会尝试创建 A Bean 的实例,进入 A 的构造器(因为 A 依赖 B,所以 Spring 会尝试创建 B 的实例)。
B Bean 初始化开始:在创建 B 时,Spring 会进入 B 的构造器,发现 B 依赖 A,此时 A 尚未完全初始化,所以 Spring 会尝试创建 A 的实例,结果又回到了第 1 步。
这时,A 和 B 的实例化相互等待,没有任何一个 Bean 能够完成初始化。这种情况下,程序进入死锁状态。
解决
Spring 在默认情况通过 三级缓存 来解决单例 Bean 的循环依赖,但这只是解决一部分问题。
三级缓存怎么帮尽量解决循环依赖问题
Spring 的 三级缓存 是指 Spring 容器在创建 Bean 时,管理了三个不同的缓存阶段,主要解决的是 循环依赖 的问题。Spring 会通过这三级缓存,避免在循环依赖中死锁。
具体流程:
一级缓存:Spring 会先尝试从一级缓存(通常是 singletonObjects)获取 Bean 实例。如果已经创建过,就直接返回该实例。
二级缓存:如果 Bean 还没有创建(即一级缓存中没有),Spring 会尝试从二级缓存(通常是 earlySingletonObjects)中获取该 Bean 实例。如果该 Bean 正在初始化中,并且有其他 Bean 依赖它,那么该 Bean 会被放入二级缓存(半成品)中。
三级缓存:如果该 Bean 还没有完成初始化,那么 Spring 会在三级缓存中保存该 Bean 的工厂(ObjectFactory)。当其他 Bean 依赖它时,可以通过工厂方法获得 Bean,从而避免了死锁问题。
@lazy 懒加载解决
当我们在 @Autowired 注解上使用 @Lazy 时,Spring 不会立即注入依赖的 Bean,而是会 推迟依赖的注入,直到该依赖真的被需要时才会注入。这样,如果 A 依赖 B,而 B 依赖 A,Spring 就不会在容器初始化时进入死锁状态,而是在需要时再去初始化其中一个 Bean。、
@Component
public class A {
private final B b;
@Autowired
@Lazy
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private final A a;
@Autowired
@Lazy
public B(A a) {
this.a = a;
}
}
好的!我们可以深入探讨一下 循环依赖 会导致死锁的情况,特别是在 Spring 中如何通过懒加载解决这个问题。
- 什么是循环依赖?
循环依赖指的是两个或多个 Bean 相互依赖的情况,形成一个闭环。例如:
Bean A 依赖 Bean B
Bean B 又依赖 Bean A
在 Spring 容器中,默认情况下,所有的 Bean 都是 单例 的,也就是说 Spring 会确保每个 Bean 只有一个实例。如果在 Bean 初始化的过程中,遇到循环依赖,Spring 就会按照默认的创建流程进行实例化,但如果没有特别的设计,可能会造成死锁或无限等待。
- 为什么循环依赖会造成死锁?
在 Spring 中,循环依赖发生时,通常是因为依赖关系的 Bean 还没有初始化完成。Spring 会先为某个 Bean 创建一个实例并开始依赖注入,但如果这个 Bean 的构造方法中或其他属性中有对另一个 Bean 的依赖,而该依赖的 Bean 还没有初始化完成,就会出现问题。
死锁的情形
假设有两个 Bean,A 和 B,它们之间有 构造器注入 关系,并且互相依赖:
java
复制代码
@Component
public class A {
private final B b;
@Autowired
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private final A a;
@Autowired
public B(A a) {
this.a = a;
}
}
在上面的代码中,A 和 B 相互依赖,且使用了 构造器注入。
- 循环依赖死锁的过程
A Bean 初始化开始:Spring 会尝试创建 A Bean 的实例,进入 A 的构造器(因为 A 依赖 B,所以 Spring 会尝试创建 B 的实例)。
B Bean 初始化开始:在创建 B 时,Spring 会进入 B 的构造器,发现 B 依赖 A,此时 A 尚未完全初始化,所以 Spring 会尝试创建 A 的实例,结果又回到了第 1 步。
这时,A 和 B 的实例化相互等待,没有任何一个 Bean 能够完成初始化。这种情况下,程序进入死锁状态。
- 如何解决循环依赖死锁?
Spring 通过 三级缓存机制 来解决 单例 Bean 的循环依赖,在这种机制下,Spring 容器会采用一种延迟初始化的方式,在 Bean 完成初始化之前 暂时返回一个代理对象,保证依赖关系能够被正常注入。
如何使用懒加载解决死锁问题?
懒加载(@Lazy)是 Spring 提供的一种 延迟加载 Bean 的机制。通过 @Lazy 注解,Spring 会推迟 Bean 的初始化,直到该 Bean 第一次被使用时才会真正创建。这样可以避免循环依赖的问题,因为懒加载的 Bean 在容器启动时不会被立即创建,直到它真的被需要的时候才会创建,从而打破循环依赖的死锁。
- 懒加载(@Lazy)如何解决循环依赖死锁?
当我们在 @Autowired 注解上使用 @Lazy 时,Spring 不会立即注入依赖的 Bean,而是会 推迟依赖的注入,直到该依赖真的被需要时才会注入。这样,如果 A 依赖 B,而 B 依赖 A,Spring 就不会在容器初始化时进入死锁状态,而是在需要时再去初始化其中一个 Bean。
懒加载解决循环依赖的例子
使用 @Lazy 注解可以推迟 Bean 的初始化,避免循环依赖的死锁:
java
复制代码
@Component
public class A {
private final B b;
@Autowired
@Lazy
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private final A a;
@Autowired
@Lazy
public B(A a) {
this.a = a;
}
}
执行流程:
A Bean 初始化开始:Spring 开始创建 A Bean 的实例。
A Bean 中的 B 依赖被延迟初始化:由于 A 中的 B 被标记为 @Lazy,Spring 不会立即初始化 B,而是把这个依赖注入的操作推迟,直到 B 被真正需要时才会去初始化 B。
B Bean 初始化开始:Spring 开始创建 B Bean 的实例。
B Bean 中的 A 依赖被延迟初始化:类似的,B 中的 A 也被标记为 @Lazy,因此 Spring 不会立即初始化 A,而是等到 A 被需要时再去初始化。
最终成功注入:由于使用了懒加载,Spring 避免了在容器启动时进行初始化的死锁,循环依赖得以打破,最终 A 和 B 可以正常完成依赖注入
使用setter方式注入
另一种解决方法是使用 setter 注入方式。通过 setter 注入,Spring 可以先创建 Bean 的实例,然后在稍后的阶段注入依赖。这种方式能够有效解决部分循环依赖问题,因为 Spring 容器先实例化 Bean,再通过 setter 方法注入依赖,而不是在构造器中就需要依赖完成。
@Component
public class A {
private B b;
@Autowired
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
@Autowired
public void setA(A a) {
this.a = a;
}
}
总结
@lazy 和 setter 方式都能解决单例模式下的循环依赖,他们都有延迟的概念
setter 注入 是在 实例化后的某个阶段 执行注入操作(即注入依赖)
而懒加载则是 推迟整个 Bean 的初始化,直到它被实际需要时才实例化。
持续更新。。。。。