深入理解spring三级缓存解决循环依赖的设计思路

news2024/9/23 3:22:49

说到这个话题的时候,很多人再熟悉不过了,因为听到太多了,而且百度一大堆,但是理解到什么程度了呢,或者说只是知道这回事,但是为什么这样设计,代码中有什么可以借鉴的,在实际业务中有什么用

问题

  • spring为什么需要三级缓存
  • spring三级缓存为什么存放的是ObejctFactory,而不是原始对象呢,这个地方不能说是bean哟,因为还早呢
  • spring为什么说使用三级缓存可以解决setter方式的注入和注解形式的单例,而比如bean的原型和构造器却无法解决

spring如何使用三级缓存

简单描述bean的创建过程

下图是bean创建的一个大体流程,先实例化bean—>填充bean里的属性—>执行实现了aware接口的实现类—>执行beanPostProcessor接口中before方法—>初始化(执行配置的initMethod方法)—>执行beanPostProcessor接口中after方法
在这里插入图片描述

其实大家都知道spring中大部分的bean都可以使用二级缓存来解决的,简单简述下流程

那么先说下三级缓存都分别存放什么东西:
在这里插入图片描述
一级缓存(单例池):完成bean,是经过完整的生命周期的包括后置处理器之后的bean
二级缓存:bean的实例化(原始对象或者代理对象)并没有经过属性填充阶段
三级缓存:bean的工厂(为什么这样做,下面有解释)

让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情景

  1. A 调用doCreateBean()创建Bean对象:由于还未创建,从第1级缓存singletonObjects查不到,就从第2级缓存中找,如果找不到那么就对A进行实例化,并存放在第二级缓存中,接下来就对A进行属性填充了,发现依赖注入的B还没有实例化,那么就需要对B去第1级缓存中找,发现肯定是没有的,因为还没存放进去呢,所以就需要从第2级缓存中找,那也是没有的,所以和A一样需要先实例化并存放到第2级缓存中,那么B还没结束呢,B已经实例化了需要进行接下来的属性填充了,发现又需要依赖A,那么就需要先去第1级缓存中找,找不到的,因为上面说了我们只是把实例化的A放到了第2级缓存中,第1级缓存肯定是没有的,所以A就能在第2级缓存中找到,那么就可以给B进行属性填充,那么B继续初始化完成一系列的后置处理,那么B就得到了完成的bean了,此时需要将完整的bean存放到第1级缓存中,并移除第2级缓存中(为什么需要移除,因为我们都是先从第1级缓存开始寻找的,如果找到那么就不会继续从第2级缓存找了,所以留着也只是浪费内存而已)
  2. 好了,上面已经创建好了完成的B的bean了,那么继续A的,A刚刚在卡在了属性填充B上,现在已经可以找到了,那么也和上面一样继续执行bean的完整生命周期了,进行初始化、后置处理器等等操作了,此时也需要将A的bean放在第1级缓存,并移除第2级缓存
    这样,循环依赖就解决了
    tips:这里注意一个点,下面可能要用到,所谓的实例化就是从磁盘加载.class文件到jvm中,通过反射调用其无参构造器实现实例化

让我们来分析一下“A的某个field或者setter依赖了B的代理对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情景

我们可以尝试去推下按照上面的步骤,因为刚开始我们肯定不会想到直接使用第3级缓存来处理,思路是一点点去优化和堆砌起来的

A 调用doCreateBean()创建Bean对象:由于还未创建,从第1级缓存singletonObjects查不到,就从第2级缓存中找,如果找不到那么就对A进行实例化创建对象,但是有个问题,这个时候我们肯定都不知道A需要循环依赖的,换句话说A依赖B,B依赖A,但是我先对A进行实例化创建对象的时候,还没到属性填充的阶段,更加不知道后面的B需要依赖A,所以这个时候我们只知道要对A进行实例化创建,那么问题就是这个时候到底是创建原始对象呢?还是代理对象呢?假设我们不管,就直接实例化,不生产代理对象,然后放到第2级缓存中,然后发现需要依赖B,实例化B,同样道理,此时我们也不知道此时B是生产代理对象发到缓存中呢?还是原始对象放到缓存中?那么我们也和A一样,直接把原始对象放到缓存中,这样下来B肯定是可以完成完整bean的生命周期的,然后需要对A进行属性填充阶段了,此时B肯定是可以从第2级缓存获取到,但是注意的是此时获取到的肯定是B的原始对象,但是问题是我们需要在A类中依赖的是B的代理对象呀,那怎么办呢?是不是走不对了,那么此时我们肯定会这样想,那既然A需要依赖注入的是B的代理对象,那么在刚刚B实例化的时候产生代理然后将代理对象存放到第2级缓存中不就可以了吗?的确是可以的,但是别忘记哈,刚刚是我们走到了后面才发现需要依赖的是代理对象呀,但是bean的生命周期不可能逆向执行吧,只能一个步骤一个步骤去执行,等你需要使用发现不对的时候,肯定不会再次执行一次了,也就是说第2级缓存无论是存放代理对象还是存放原始对象都无法解决,而且如果都提前在第2级缓存中存放所有的代理对象,会出现2个问题①违反了spring bean创建的生命周期设计初衷,我们都知道,bean产生代理是在初始化之后的后置处理器②就算所有的代理操作都提前到实例化阶段了,性能就很差,因为生产代理本身就耗费性能的,所以只有有需要的情况下才不得不去产生代理,如果已经生成了代理对象之后,在初始化的时候进行后置处理器中就不会再次生成了,会进行判断的,如果有代理对象就跳过,因为第3级缓存存放的是对象工厂在整个bean加载中只能调用getObject()一次,否则每次调用就会产生不同的代理对象

有代理情况存在的循环依赖了,此时就需要第3级缓存了,这个缓存就专门来处理代理对象的存放

  1. 按照上面的思路我们使用三级缓存来处理下如何解决呢?A 调用doCreateBean()创建Bean对象:由于还未创建,从第1级缓存singletonObjects查不到,此时只是一个半成品(提前暴露的对象),放入第3级缓存singletonFactories(这个地方只是存放获取对象工厂,刚刚上面也分析此时你并不知道是后续是需要代理对象还是原始对象)
  2. A在属性填充时发现自己需要B对象,但是在三级缓存中均未发现B,于是创建B的半成品,放入第3级缓存singletonFactories
  3. B在属性填充时发现自己需要A对象,从第1级缓存singletonObjects和第2级缓存earlySingletonObjects中未发现A,但是在第3级缓存singletonFactories中发现A,将A放入第2级缓存earlySingletonObjects,同时从第3级缓存singletonFactories删除
  4. 将A注入到对象B中
  5. B完成属性填充,执行初始化方法,将自己放入第1级缓存singletonObjects中(此时B是一个完整的对象),同时从第3级缓存singletonFactories****和第2级缓存earlySingletonObjects中删除
  6. A得到“对象B的完整实例”,将B注入到A中
  7. A完成属性填充,执行初始化方法,并放入到第1级缓存singletonObjects

一张图解释下:
在这里插入图片描述

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
        //第1级缓存 用于存放 已经属性赋值、完成初始化的 单列BEAN
        private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
        //第2级缓存 用于存在已经实例化,还未做代理属性赋值操作的 单例BEAN
        private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
        //第3级缓存 存储创建单例BEAN的工厂
        private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
        //已经注册的单例池里的beanName
        private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
        //正在创建中的beanName集合
        private final Set<String> singletonsCurrentlyInCreation =
                Collections.newSetFromMap(new ConcurrentHashMap<>(16));
        //缓存查找bean  如果第1级缓存没有,那么从第2级缓存获取。如果第2级缓存也没有,那么从第3级缓存创建,并放入第2级缓存。
        protected Object getSingleton(String beanName, boolean allowEarlyReference) {
            Object singletonObject = this.singletonObjects.get(beanName); //第1级
            if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
                synchronized (this.singletonObjects) {
                    singletonObject = this.earlySingletonObjects.get(beanName); //第2级
                    if (singletonObject == null && allowEarlyReference) {
                        //第3级缓存  在doCreateBean中创建了bean的实例后,封装ObjectFactory放入缓存的bean实例
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            //创建未赋值的bean
                            singletonObject = singletonFactory.getObject();
                            //放入到第2级缓存
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            //从第3级缓存删除
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
            return singletonObject;
        }   
    }

spring为什么无法解决bean的原型或构造器的循环依赖

其实在上面的文字描述中提到过,我们都知道bean刚开始需要进行实例化,而所谓的实例化就是从磁盘加载.class文件到jvm中,通过反射调用其无参构造器实现实例化,如果是有参构造器就会出现在实例化阶段就无法完成了,还没到缓存阶段就歇菜了,spring三级缓存设计都是利用提前暴露的实例化对象或者代理对象,如果都无法实例化那就没办法使用三级缓存解决了;bean都是单例的才能放到缓存中,试想下缓存的结构都是map结构,key都是beanName肯定是唯一的,如果不是单例的,也就没有意义了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/188272.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jmeter和JDK下载安装及环境变量配置详细教程

写在最前面的话&#xff0c;jmeter安装部署依赖java环境&#xff0c;所以得安装JDK java环境检查 命令行输入&#xff1a;java -version 如果出现以下内容&#xff0c;说明java环境已安装&#xff0c;无需理会&#xff0c;如果没有&#xff0c;需要安装JDK 一、下载并安装JDK…

PSP模拟器截图CG的高清化-Waifu2x

由PSP游戏本体提取CG图片直接超分自然是比较舒服的&#xff0c;但实际上因了加密等诸多问题&#xff0c;甚或不如直接模拟器截了图进行处理来得方便 1. 模拟器设置 如果要截图的话&#xff0c;自然是以得到更好的效果为宜&#xff0c;于是可以对模拟器进行一些基本的设置。 对…

Java两大工具库:Commons和Guava(3)

您好&#xff0c;我是湘王&#xff0c;这是我的CSDN博客。值此新春佳节&#xff0c;我给您拜年啦&#xff5e;祝您在新的一年中所求皆所愿&#xff0c;所行皆坦途&#xff0c;展宏“兔”&#xff0c;有钱“兔”&#xff0c;多喜乐&#xff0c;常安宁&#xff01;虽然Apache Com…

【33】C语言 | 联合体详解

目录 1、联合类型的定义 2、联合的特点 3、联合大小的计算 1、联合类型的定义 联合也是一种特殊的自定义类型这种类型定义的变量也包含一系列的成员&#xff0c;特征是这些成员公用同一块空间《所以联合也叫共用体)先看下面代码输出什么&#xff1f; union Un {char c;int …

我用css3为好友胡歌的宝宝做了一个动画照片墙

软件人生风雨十年&#xff0c;仙剑一过去也有十年有余了&#xff0c;和胡歌认识那么久&#xff0c;今日喜闻好友胡歌生宝宝的消息&#xff0c;惊喜之余用css3为胡歌的宝宝做了动画照片墙的模板效果。 目录 1. 实现思路 2. 墙体的实现 3. 选取模板素材&#xff0c;进行图片元…

虹科案例 | 石油天然气行业CFD高性能计算解决方案

公司简介 DNV GL 是全球领先的能源、石油和海事行业风险管理及资产绩效提升的软件供应商&#xff0c;主要为客户提供全面的风险管理和各类评估认证服务&#xff0c;认证涉及信息通信技术、汽车及航空天、食品与饮料、医疗等方面。 DNV GL以让世界更安全、更智能、更环保为使命…

随笔记——MQ

文章目录1、 概要2、 为什么使用MQ/使用MQ的好处&#xff1f;3、 使用MQ的缺点&#xff1f;4、 使用MQ会产生的问题及解决办法&#xff1f;4.1、如何保证消息的顺序性&#xff1f;4.2、如何解决消息被重复消费&#xff1f;4.3、如何解决消息丢失&#xff1f;4.4、如何解决消息积…

基于turtle实现的新冠疫情传播模拟 附完整代码可直接运行

代码运行视频参考:https://www.bilibili.com/video/BV1hR4y1h7Te/?spm_id_from=333.999.0.0&vd_source=8f3cf4ad6c08a40d40ca6809c9c9e8ca 高阶版运行结果展示

基于Android的家校互动系统app

需求信息&#xff1a; 功能需求: 1&#xff1a;发通知、发作业 发通知和发作业&#xff0c;是学校教师特有的功能&#xff0c;教师可以通过平台进行通知和作业的发放&#xff0c;每当发一条信息&#xff0c;该班的所有人员便可以收到来自服务器的信息推送&#xff0c;提醒家长打…

经济下行压力下的销售行业,将数据效益最大化方能立于不败之地

2022年9月&#xff0c;世界经济论坛发布《首席经济学家展望》报告&#xff0c;并指出“当前每10位经济学家中&#xff0c;就有7位认为全球经济发生了一定程度的衰退。” 在整体下行的经济环境中&#xff0c;由于销售人员会获悉客户削减预算&#xff0c;推迟采购&#xff0c;或…

mysql服务nginx和firewalld代理实现

文章目录环境准备nginx代理mysql服务linux防火墙实现mysql流量转发工作中常常遇到只有某个特定服务器才能访问数据库的情况&#xff0c;这个时候为了解决团队同时访问数据库的问题可以采用nginx代理和linux防火墙流量转发的方式解决。实战测试如下&#xff1a;环境准备 准备一…

做题关键点--位操作符

x |&#xff08;x1&#xff09;去掉从右开始的第一个0 a^a0&#xff0c;异或遵循交换律&#xff0c;即只要存在偶数个a&#xff0c;这部分结果为0. a^0a 与0异或为本身 寻找奇数_牛客题霸_牛客网 不用加减乘除做加法_牛客题霸_牛客网 二进制进行加法运算时与十进制的思路是…

Qt之QtConcurrent无需使用低级线程原语编写多线程

几个要点&#xff1a; 通过QtConcurrent::run()返回的QFuture不支持取消、暂停&#xff0c;返回的QFuture只能用于查询函数的运行/完成状态和返回值。 请注意&#xff1a;该函数可能不会立即运行; 函数只有在线程可用时才会运行。 导入模块 在 C API changes 有关于 Qt Concu…

Linux常用命令——rename命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) rename 用字符串替换的方式批量改变文件名 补充说明 rename命令用字符串替换的方式批量改变文件名。 语法 rename(参数)参数 原字符串&#xff1a;将文件名需要替换的字符串&#xff1b; 目标字符串&#x…

GuLi商城-人人开源搭建前端项目

人人开源&#xff1a; 下载到本地并解压&#xff1a; VsCode打开解压好的项目&#xff1a; MAC安装Nodejs&#xff0c;略 参考&#xff1a;MAC(m1)-安装Nodejs_ZHOU_VIP的博客-CSDN博客 MAC打开终端&#xff1a; npm install类似maven下载所有的组件 报错&#xff1a; 解…

Vue基础10之插件、scoped与lang的样式

Vue基础10插件scoped与lang样式scopedlang样式安装lessless样式的使用总结插件 plugins.js: export default {install(Vue){console.log("Vue:",Vue)//全局过滤器Vue.filter(mySlice,function (value){return value.slice(0,5)})//定义全局指令Vue.directive(fbind,…

【数据结构】8.6 基数排序

文章目录基数排序定义基数排序算法基数排序算法分析基数排序定义 前述的各类排序方法都是建立在关键字啊比较的基础上&#xff0c;而分配类排序不需要比较关键字的大小&#xff0c;它是根据关键字中各位的值&#xff0c;通过对待排序记录进行若干趟分配与收集来实现排序的&…

微信小程序 Springboot卫生应急培训报名系统java

本文以微信开发者、Springboot框架、java为开发技术&#xff0c;实现了一个卫生应急培训小程序。卫生应急培训小程序的主要使用者分为管理员服务端和用户客户端&#xff0c;其中管理员服务端权限&#xff1a;首页、个人中心、用户管理、通知公告管理、在线学习管理、培训管理、…

中国电子学会2021年09月份青少年软件编程Python等级考试试卷一级真题(含答案)

青少年软件编程&#xff08;Python&#xff09;等级考试试卷&#xff08;一级&#xff09; 分数&#xff1a;100.00 题数&#xff1a;37 一、单选题&#xff08;共25题&#xff0c;每题2分&#xff0c;共50分&#xff09; 1. 取整除的运算符是&#xff1f;&#x…

每天一道大厂SQL题【Day02】

每天一道大厂SQL题【Day02】 大家好&#xff0c;我是Maynor。相信大家和我一样&#xff0c;都有一个大厂梦&#xff0c;作为一名资深大数据选手&#xff0c;深知SQL重要性&#xff0c;接下来我准备用100天时间&#xff0c;基于大数据岗面试中的经典SQL题&#xff0c;以每日1题…