A 类中有一个属性 B ,也就是说 A 依赖 B,同时 B 类中有一个属性 A, 也就是说 B 依赖 A. 他们之间的依赖关系形成了环。就是我们说的循环依赖,如下图:
循环依赖示例
public class CircularDependenciesDemo {
public static void main(String[] args) {
new A1();
}
}
class A1 {
private B1 b1;
public A1() {
this.b1 = new B1();
}
}
class B1 {
private A1 a1;
public B1() {
this.a1 = new A1();
}
}
结果:
Exception in thread "main" java.lang.StackOverflowError
at io.dc.B1.<init>(CircularDependenciesDemo.java:23)
at io.dc.A1.<init>(CircularDependenciesDemo.java:16)
at io.dc.B1.<init>(CircularDependenciesDemo.java:24)
如上所示,发生 栈溢出错误。下面我们试着解决这种循环依赖
解决循环依赖
public class CircularDependenciesDemo {
// 实例缓存池
public static Map<String, Object> existsObject = new HashMap<>();
public static Object loadObject (String objectName) {
if(existsObject.containsKey(objectName)) {
return existsObject.get(objectName);
}
if("A1".equals(objectName)) {
A1 a1 = new A1();
// 先将空白的实例放入缓存
existsObject.put("A1",a1);
// 然后将对该空白实例进行属性赋值
a1.setB1((B1)loadObject("B1"));
return a1;
}
if ("B1".equals(objectName)) {
B1 b1 = new B1();
existsObject.put("B1",b1);
b1.setA1((A1)loadObject("A1"));
return b1;
}
return null;
}
public static void main(String[] args) {
A1 a1 = (A1)loadObject("A1");
a1.getB1().sayHello();
}
}
class A1 {
private B1 b1;
public B1 getB1() {
return b1;
}
public void setB1(B1 b1) {
this.b1 = b1;
}
}
class B1 {
private A1 a1;
public A1 getA1() {
return a1;
}
public void setA1(A1 a1) {
this.a1 = a1;
}
public void sayHello(){
System.out.println("我是 B1 , 你好");
}
}
什么是循环依赖
A 类中有一个属性 B ,也就是说 A 依赖 B,同时 B 类中有一个属性 A, 也就是说 B 依赖 A. 他们之间的依赖关系形成了环。就是我们说的循环依赖,如下图:
循环依赖示例
结果:
如上所示,发生 栈溢出错误。下面我们试着解决这种循环依赖
解决循环依赖
结果:
本来由构造方法来构造 A1 中的 B1. 现在分成两步:
为了解决循环依赖,设置了一个实例缓存池,existsObject. 用来存放已经生成的实例。但是这个实例池会存放空白对象的状态。在多线程的情况下,会取到一个空白实例。也就是对象中的字段都是 null, 引发程序错误。我们可以再添加一层二级缓存,二级缓存中存放空白实例。一级缓存中只放完整实例。
纯净的缓存
通过添加二级缓存,把空白对象和完整对象剥离了。当从一级缓存中取实例时,要么拿到的是完整对象,要么拿到的是 null, 而不会获取到空白的对象,引发错误。
但是上面的代码是有问题的,当实例未初始化完,并且在在多线程的情况下,仍然会取到不完整对象,如下:
那么单单只加二级缓存并不能解决这个并发问题,同时还要加锁:
到此我们才完整的解决了循环依赖,并且保证不会取到不完整的实例.
其实 spring 也是这样来解决bean循环依赖的,不过 spring 还添加动态代理bean的功能,为了解耦,还添加了三级缓存,保证代码整洁,优雅。
spring 是如何解决循环依赖的
由于 spring 在处理循环依赖时考虑很多其他功能,代码非常复杂,为了便于展示,这里只模仿核心功能:
详细说明已在注释中
为什么需要三级缓存,而不是两级缓存
我认为两级缓存完全可以解决循环依赖,完全可以先创建 bean的动态代理放入二级缓存中,而不是在 getSingleton 方法中延迟调用三级缓存中的 lambda 表达式再去生成动态代理。
我认为三级缓存作用之一是为了代码解耦,逻辑统一。
spring 如何避免拿到不完整的bean
spring 没有解决的循环依赖