学习笔记——spring循环依赖

前言

主要讲诉的解决spring循环依赖的逻辑和思想,并不打算从springboot具体的代码实现角度,以后研究整理过后再尝试补充。

什么是spring的循环依赖

首先申明两个bean的资源类,BeanA和BeanB,所谓spring的循环依赖就是,BeanA类里有一个字段属性是自动装配的BeanB,同样BeanB类的一个字段属性是自动装配的BeanA。这样在spring对bean初始化管理的时候,就会造成循环依赖现象。
这里先补充一下bean的初始化的一个过程,这里的过程省略掉很多细节,只是简单的抽象说明一下,获取beanDefinition对象后,开始bean的实例化,实例化完成后,接着对bean的实例进行属性注入,属性注入完成后,对属性注入了的bean实例进行初始化。
上面的叙述,是不是感觉有问题?怎么会bean都实例化了,到了后面又进行初始化,我当时真的是RLGL,这里我来解释一下,一开始的实例化的意思,对应着jvm先初始化,分配空间,设置初始默认值,然后new对象,实例化。这样的一个完整的过程,后面属性注入后的初始化的意思,已经不是对bean实例分配空间和默认值,而是去实现一些附带的方法,如init-method等。
循环依赖现象出现在属性注入的这个阶段,但beanA进行实例化以后,开始进行属性注入,到属性字段BeanB的时候,发现没有对应的BeanB,这时会对BeanB进行初始化,BeanB初始化到了属性注入的时候,又会去找BeanA,发现BeanA没有,毕竟此刻的BeanA还在等BeanB初始化呢,这样一来就会一直找,就会造成死循环,导致栈溢出,报错。

怎么解决循环依赖

循环依赖产生的原因是,在属性注入的时候发现字段属性的Bean没有实例对象,那么,就得在属性注入之前,先对bean先实例化,这也就是上面bean的初始化顺序的来由,当然,总不能初始化一次含目标属性的值,我就实例化一次对象吧,所以得找一个缓存存一下,当下次用到得时候,直接从缓存去取不就好了。由此,一级缓存的概念出来了。在属性注入之前,先获取一下实例(这里的属性注入和实例获取都是都过反射资源类的class对象),然后存入一个一级缓存map里,key为类名,value为对应的实例,这样在获取bean的时候,先去一级缓存判断一下,是否存在,有就返回,无责获取。
那么流程变成了这样,BeanA先实例化,存入一级缓存,然后BeanA进行属性注入BeanB,然后去找BeanB的实例,发现没有,就创建BeanB的实例,BeanB属性注入BeanA,发现一级缓存中存在BeanA的实例,BeanB属性注入完成,执行初始化以后,便返回BeanA的属性注入,然后BeanA的属性注入完成。这样BeanB和BeanA都完成了属性注入,BeanB先完成,这里暂且不要去考量属性注入完成后的初始化执行了啥玩意。自此,加入一级缓存,完美解决了循环依赖问题。
但是新的问题产生了,在并发的情况下,例如;当线程1去getBean的时候,beanA走到了实例化后,就放入一级缓存,尚未来得及属性注入的时候,线程2来获取beanA,此时获取到的是,一级缓存中尚未来得及进行属性注入的不完整的beanA。

并发情况下的获取的bean不完整的问题

并发并发,那就加锁,把整个bean的获取都给锁着,只要完成属性注入和初始化后,我才释放锁,那么就能保证在其他线程获取的时候,获取的bean一定是属性注入完成的。现在循环依赖和并发的情况都解决了,貌似万事大吉,其实不然,加锁带来的新问题,并发下beanA已经初始化完成,此时正在初始化其他的bean,此刻我另外的线程想要来获取已经初始化完成的beanA,获取不了,被锁住了,得等到当前bean得初始化完成,然后再去获得或者竞争锁,大大的降低效率。咋办,引入二级缓存来解决效率问题,那为什么会引入二级缓存就能去解决效率问题的逻辑思想,目前我理解的太浅,没办法提供这个思路。

二级缓存解决效率问题

尝试二级缓存来解决效率的问题。在属性注入后,将实例化后完成属性注入的bean存入一级缓存,在实例化后,将未完成属性注入的bean存入二级缓存。然后继续对二级缓存的判断到初始化完成进行加锁。
走一遍流程,对BeanA初始化,首先判断一级缓存有无,有则返回,无则继续,然后对二级缓存进行判断,有则返回,无责继续,一开始都是无,接下来对BeanA实例化,然后存入二级缓存,接着对BeanA属性注入,此时发现BeanB没有,开始对BeanB的初始化,一级缓存判断没有,继续二级缓存判断没有,对BeanB实例化,BeanB的实例存入二级缓存,BeanB开始属性注入,此时去找BeanA,发现二级缓存中有实例,返回BeanA的实例,BeanB的属性注入完成,存入一级缓存,返回BeanB的属性注入后的实例,BeanA完成属性注入,存入一级缓存,并返回BeanA的属性注入后的实例存入单例池。
设想,此刻去获取BeanA,发现一级缓存有值,直接返回,若一级缓存无值,则开始下面的初始化步骤,进行BeanA的初始化,直到完成。所以,当其他线程获取已经初始化好的Bean时,去一级缓存中取即可,因为只锁了二级缓存,不会造成阻塞的同时,又保证了不会获取不完成的bean。此时完美解决spring的循环依赖和锁导致的效率底下问题。

结语

其实这个地方的知识点,还是得和代码,一边写着,一边来进行讲解,由于作者心中自成代码,所以觉得逻辑顺畅,但是,读者可能一脸蒙蔽,之所以先这样写,以实记录思路的同时,二是为将来更加细致的讲解做准备,先小试牛刀一下。毕竟还有一个很关键的问题,就是spring的aop问题,生成的bean的转化为代理对象,毕竟aop的底层是动态代理。这里用三级环境来实现这个问题,也不是说二级缓存实现不了,只是二级缓存会存在效率问题,而且,这里我不打算讲的一个原因是,spring初始化的bean要生成代理对象的这个概念我暂时没搞清楚,以上文章都只是为最后的springboot这个框架做准备,最后作者将会整合整理出springboot这个框架的条条蔓蔓,尽情期待。
补充:想了想,还是觉得把三级缓存来解决生成代理对象的逻辑讲解一下,二级缓存之所以会存在效率问题,因为如果生成实例后直接转换为代理对象存入二级缓存,那么即可保证返回得bean已经是一个代理对象了,倘若同一个bean被依赖一次以上,则会创建不止一个代理对象,占内存影响性能。解决思路,创建三级缓存来将实例转化为代理对象存入二级缓存,贼每次要取得时候,去二级缓存找一下是否创建代理对象,这样的话,就保证无论依赖多少次,都是一个代理对象。按这个思路走一遍流程,获取beanA,123级缓存都没有,创建实例,三级缓存存入,key为beanname,value为一个函数式接口,这个接口方法用来创建代理对象,然后beanA开始属性注入,发现beanB没有初始化,开始初始化beanB,一二三级缓存都没有,创建beanB的实例,同样存入三级缓存,beanB开始属性注入,一级缓存没有,二级缓存存在,直接返回。beanB属性注入完成,存入一级缓存,继续初始化完成,返回beanB,然后beanA属性注入完成,存入一级缓存。但是,这里二级缓存怎么存在的问题,就要在三级缓存的判断里面实现了,继续思考,一级缓存和二级缓存的判断不变,都是有则返回,无责继续,三级缓存判断,有则返回函数式接口对象,调用函数式接口方法生成代理对象,存入二级缓存。然后实现,最终代码流程如下,beanA开始初始化,在beanA实例化后,三级缓存存入beanA,属性注入初始化beanB,三级缓存存入beanB,继续属性注入beanA,发现都一二级缓存没有,此时三级缓存找到,存入二级缓存,然后属性注入beanB,又去初始化beanB,同样二级缓存存入,属性注入beanA,这时发现二级缓存有beanA,完成属性注入,存入一级缓存和单例池并返回。beanA也完成属性注入,存入一级缓存并返回。有一说一,确实挺绕的,想要一起交流学习的加qq:1192375101,备注csdn交流学习吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值