阿金
curatorjin
早起赶画稿,熬夜写项目
disciple_sneaker@163.com
相关标签: Spring 框架 编程思维

从Spring三级缓存看循环依赖

什么是循环依赖

循环依赖是指在软件开发中,两个或多个模块相互依赖,形成一个闭环的情况。

这个问题似乎用这样一句简单的话就已经可以解释的足够清楚了,实际上这个词并不是一个编程专有名词,而是一种逻辑上的有趣现象——一件事物依赖于另一件事物形成了依赖的链条,最终形成环状。早期的贴吧时代,Java吧、C++吧、C语言吧的说明就是类似的逻辑。

简单来说,循环依赖就是依赖关系出现了环形,没有了明确的起始点。

这篇文章并不想谈及具体的编程场景,只是想重点关注这种有趣的逻辑现象以及Spring框架对于此种场景的处理方式。

循环依赖的“毁灭性”

到底是先有鸡?还是先有蛋?

对于人类的大脑而言,我们很容易就能接受循环依赖的逻辑,至少不会因为到底是先有鸡还是先有蛋的问题导致生活不能自理。但是对于程序来说不是这样,这也是为什么这类问题在程序中显得尤为刺眼。

这里我想提一下死锁的现象,死锁是指在多个线程的执行过程中,因为资源的争夺而造成的一种僵局。广义上来说死锁同样是一种循环依赖:线程A依赖线程B所占有的资源才能继续进行,而线程B依赖线程A所占有的资源才能继续进行。程序的逻辑处理始终是线性的,至少目前还没有一种可以倒着走的钟,这也是“环”对于程序的毁灭所在。

考虑到后面要谈Spring,这里就重点关注一下Spring中循环依赖的困境。Spring的核心思想是控制反转(IOC)——由程序代替程序员控制程序中对象的生命周期,这里可以回顾一下关于面向对象的思维。当我们的程序设计中出现了循环依赖的情况,就意味着程序需要实现一段环形的逻辑。例如我们的对象A中依赖了对象B,对象B中依赖了对象A,那么在Spring进行对象A的实例化时,会发现需要先实例化对象B,然而实例化对象B又需要先实例化对象A,于是程序就因为这个到底是先有鸡(对象A)还是先有蛋(对象B)的问题呆在原地,抛出异常——

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'objectA': Bean with name 'objectA' has been injected into other beans [objectB] in its raw version as part of a circular reference, but has eventually been wrapped.
This means that said other beans do not use the final version of the bean.
This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
    
// 有死锁那味了吧

Spring的处理方式

你的意思是,要在单行道里走个圈还不逆行是吧?

首先看上一节中提到的异常的处理,虽然我认为上一节中也解释的差不多了:

  1. Spring走Bean的实例化流程尝试创建A的实例 ,在创建实例之前先从 “正在创建Bean池” (一个缓存Map)中去查找A是否正在创建,如果没找到,则将A放入“正在创建Bean池”中,然后准备实例化构造器参数B。
  2. Spring走Bean的实例化流程尝试创建B的实例,在创建实例之间先从“正在创建Bean池”中去查找B是否正在创建,如果没找到,则将B放入“正在创建Bean池”中,然后准备实例化构造器参数A。
  3. Spring走Bean的实例化流程尝试创建A的实例,在创建实例之间先从“正在创建Bean池”中去查找A是否正在创建。
  4. Spring发现A正处于“正在创建Bean池”,表示出现构造器循环依赖,抛出异常:“BeanCurrentlyInCreationException”

如果要在一个线性处理的流程中解决这个环形的逻辑,就只有先解开它——==在创建到需要创建的对象时,先视作已完成,再后续的步骤中再回过头补全==。

把这个思路应用到创建Bean的过程中来看,在我们创建Bean A时,由于没有找到创建好的Bean B,此时A被保存到这个正在创建的Bean缓存池中,为了预防这个Bean A在后续的创建Bean过程中被再次依赖,我们将这个Bean A的半成品缓存起来暴露引用,也就是上文说到的视作已完成。后续创建Bean B的过程发现了需要Bean A作为依赖,此时我们回头去找的时候找到了这个半成品,于是Bean B的创建可以继续下去。后续的Bean A也会因为B的创建而完成。

三级缓存

这怎么多了一个零件

在上一节的讨论里,我们似乎只用了两层缓存来解决循环依赖的问题:正在创建的Bean缓存池半成品缓存池。Spring中为什么要用到三级缓存呢?

首先我们知道,Spring的核心思想是IOC(控制反转),由程序来控制对象的生成和销毁。那么从设计的动机来分析,我们先看上文提到的这两个缓存:

  1. 正在创建的Bean缓存池:保证了Bean的单例性,不会出现同一个Bean重复创建的情况。
  2. 半成品缓存池:保证了不会因为循环引用而导致Bean无法创建。

这个流程中还有一个问题:创建Bean B的时候,拿到的A只是一个半成品,需要在后续的步骤中补全。而这个后续的步骤中,Bean对象的引用可能还会发生改变,可以参考Spring的AOP(面向切面)。所以这一个额外的缓存就是为了保证在这个过程中,Bean的引用不会发生变化。当然,这和我们这篇讨论的循坏依赖是两个问题了。

循环依赖、递归与自指

数学是不完备的,数学是不一致的

这三个都是十分“有趣”的逻辑话题,甚至在谷歌搜索“递归”时,还会给你一个形象的解释。而自指则是在诸多有趣的悖论中有所体现,例如著名的罗素悖论:==如果一个理发师只给不给自己理发的人理发,那么他会给自己理发吗?==包括本文讨论的循环依赖问题,实际上都是自指的现象。

可不要小看了这个“自指”,甚至数学的大厦都差点因为它塌了。程序的运行是依托于数学逻辑并严格运行的,这就意味着程序在面对自指类的逻辑时也会崩溃,至少目前是这样。可能你会说,我们刚刚不是解决了循环依赖的问题了吗?实际上我们解决的只是处理逻辑,让整个程序在这种互相依赖的方式下正常运行。相当于只是让程序描述了一遍理发师的悖论,并没有真正让这位理发师去理发。只要他不去想给自己理发这件事情,一切都很正常。

Spring已经解决了循环依赖问题,所以我们在设计程序时就可以随意的出现循环依赖的情况吗?我始终认为,循环依赖或是其他的自指逻辑都是在编程时应当尽力避免的。一个好看的结构应当是有层次的,有逻辑递进关系的。这里不得不感叹,我们人脑可以很轻松的接受自指的逻辑,并且能进行更多的类似的描述创作。或许到了程序可以接受并理解自指的时候,就是真正实现人工智能的时候了。

相关链接