Spring是如何解决循环依赖

ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花

一、循环依赖基础认知

循环依赖是指两个或多个Bean之间形成相互依赖的闭环,比如Bean A依赖Bean B,Bean B同时依赖Bean A,若没有特殊处理,容器会陷入无限创建Bean的死循环,最终抛出创建异常。Spring针对循环依赖做了针对性适配,但并非所有循环依赖都能解决,核心仅支持单例Bean的setter/属性注入循环依赖

1.1 循环依赖常见分类

  • 构造器循环依赖:通过构造方法注入依赖,Bean实例化阶段就需要依赖对象,无法打破闭环,Spring无法解决
  • setter/属性注入循环依赖:Bean先完成实例化,再通过set方法/字段注入依赖,Spring可通过三级缓存解决
  • 原型作用域循环依赖:每次获取原型Bean都会创建新实例,无缓存机制,Spring无法解决

Spring解决循环依赖的核心思路:提前暴露未完全初始化的Bean早期引用,让依赖方先获取引用完成注入,后续再补全Bean的初始化逻辑,打破闭环死锁。

二、Spring解决循环依赖的核心:三级缓存机制

Spring容器在DefaultSingletonBeanRegistry类中维护了三级缓存(三个ConcurrentHashMap),分层存储不同生命周期阶段的Bean,通过缓存协同工作完成依赖注入,三级缓存各司其职、逐级降级获取Bean实例。

2.1 三级缓存详细说明

缓存级别

源码变量名

存储内容

核心作用

一级缓存(成品缓存)

singletonObjects

完全初始化完毕、可直接使用的单例Bean

存放最终成品Bean,供全局获取,保证单例唯一性

二级缓存(半成品缓存)

earlySingletonObjects

已实例化、未完成初始化的早期Bean(原始对象/代理对象)

缓存提前暴露的早期引用,避免重复创建早期Bean,解决普通循环依赖

三级缓存(工厂缓存)

singletonFactories

Bean工厂对象(ObjectFactory),延迟生成早期Bean/代理对象

解决AOP代理场景下的循环依赖,保证代理对象唯一性,避免原始对象与代理对象不一致

三、循环依赖解决全流程(以A依赖B、B依赖A为例)

以下是Spring创建单例Bean A、B并解决循环依赖的完整步骤,贴合源码执行逻辑,清晰体现三级缓存的流转过程:

步骤1:容器启动,开始创建Bean A

调用getSingleton(A)尝试从一级缓存获取A,此时A未创建,进入doCreateBean方法。先通过构造器实例化A(仅分配内存,属性未赋值、初始化未执行),得到A的原始半成品对象

步骤2:提前暴露A的工厂对象到三级缓存

实例化完成后,Spring会将A的ObjectFactory工厂存入三级缓存singletonFactories,标记A已进入实例化阶段,允许其他Bean提前获取A的引用。此时A仅完成实例化,未进行属性填充和初始化。

步骤3:填充A的属性,触发Bean B的创建

执行属性填充逻辑,发现A依赖B,转而调用getSingleton(B)创建B。重复步骤1-2:实例化B,将B的工厂对象存入三级缓存,随后填充B的属性,发现B依赖A。

步骤4:B从三级缓存获取A的早期引用

B填充属性需要A,调用getSingleton(A):先查一级缓存无A,再查二级缓存无A,最后从三级缓存获取A的工厂对象,调用getObject()生成A的早期引用(普通场景为原始对象,AOP场景为代理对象)。

生成后,将A的早期引用移入二级缓存earlySingletonObjects,并删除三级缓存中A的工厂对象,避免重复生成。B成功获取A的早期引用,完成自身属性填充和初始化,最终存入一级缓存。

核心答疑:A、B互相依赖,为何B不进二级缓存?看似双向依赖,但依赖获取的时机完全不同,这是判断是否进二级缓存的唯一标准:✅ B获取A的时机:A仅完成实例化、还在属性填充阶段,属于未初始化的半成品,没有成品A可用,必须提取A的早期引用,因此A需要存入二级缓存过渡;❌ A获取B的时机:A回头找B时,B已经完成属性填充+初始化全流程,是直接存入一级缓存的成品Bean,A无需调用B的半成品引用,自然不需要把B放入二级缓存。简言之:二级缓存只救“自身没完工,就被别人强行索要引用”的Bean,B是顺利完工的成品,无需二级缓存兜底。

步骤5:A完成属性填充和初始化

B创建完成后,A顺利获取B的成品对象,完成自身属性填充、初始化方法执行等后续逻辑。随后将A从二级缓存移入一级缓存,删除三级缓存中残留数据,A最终成为成品Bean。

步骤6:闭环完成

此时A和B均存入一级缓存,相互持有对方的合法引用,循环依赖闭环成功打破,容器正常启动。

二级缓存准入铁律:谁先被“中途截胡”,谁才进二级缓存。循环依赖中,只有先创建、先被对方提前依赖的半成品Bean(如本例A)需要二级缓存;后创建、顺利完工的Bean(如本例B)直接晋级一级缓存,全程不接触二级。

四、关键设计:为什么必须用三级缓存?

很多开发者疑惑二级缓存能否解决循环依赖,答案是:普通无AOP的循环依赖用二级缓存即可,但AOP代理场景必须用三级缓存

  • 若只用二级缓存:AOP代理会在Bean初始化后生成,循环依赖时B获取的是A的原始对象,而容器最终存放的是A的代理对象,导致对象不一致,破坏单例原则
  • 三级缓存的ObjectFactory实现延迟代理:只有发生循环依赖时,才通过工厂生成代理对象并放入二级缓存;无循环依赖时,代理对象在初始化后生成,保证逻辑一致性

五、Spring无法解决的循环依赖场景

以下场景即便开启缓存机制,Spring仍无法处理,会直接抛出BeanCurrentlyInCreationException异常:

  • 构造器注入的循环依赖:实例化阶段就需要依赖对象,无法提前暴露引用,彻底无法解决
  • 原型(prototype)作用域循环依赖:原型Bean无缓存,每次获取都会创建新实例,无法形成缓存闭环
  • @DependsOn强制循环依赖:注解强制指定Bean创建顺序,形成死锁,容器直接校验失败
  • 多线程并发创建单例Bean:并发场景下缓存读写冲突,可能导致早期引用异常

六、总结

Spring解决循环依赖的核心是三级缓存+提前暴露早期引用,仅适配单例Bean的setter/属性注入场景,通过分层缓存实现Bean生命周期的解耦,既保证了单例唯一性,又兼容AOP代理逻辑。日常开发中,应尽量避免构造器循环依赖、原型循环依赖等违规场景,减少容器启动风险。

ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花

Spring 文章被收录于专栏

本专栏聚焦Spring全生态体系,从IoC/AOP核心原理入手,覆盖Spring Boot自动配置、事务管理、Web开发等实战内容。拆解循环依赖、动态代理等高频面试难点,助力开发者从入门到精通,打通单体到微服务的技术链路,解决企业级开发痛点,提升架构设计与问题排查能力,成为Java后端进阶的必备技术专栏。

全部评论

相关推荐

1️⃣讲一下强化学习 reward 函数设计。(1)可验证奖励( RLVR ):对有确定答案的任务(数学、代码),用 ground - truth 结果验证(正确+1,错误0/-1)。(2)奖励模型( RM - based Reward ):对主观任务(对话、写作),训练 RM 从偏好数据学习打分。(3)规则奖励:对特定安全/格式要求,用硬规则直接判断。2️⃣现有 Embedding 模型相比 CLIP 的区别?(1)训练目标不同: CLIP 用 InfoNCE 做图文对比,目标是让匹配图文对相似;专用模型用 SimCSE /三元组损失/ NLI 监督/多任务,目标是精确捕捉文本语义的细粒度差异。(2)数据质量不同:专用模型训练于高质量语义标注对,语义标注精准; CLIP 训练于网络爬取图文对,文本侧噪声大、以短标题为主,不利于文本语义建模。(3)性能对比: MTEB 基准上, BGE - Base /E5- Base 等在文本检索、语义相似度等任务上大幅领先 CLIP ;但 CLIP 在图文检索上仍有独特优势。3️⃣ GRPO 和 PPO 的区别。(1)架构差异: PPO 需要四个模型(策略模型π t _0、旧策略、奖励模型 RM 、价值模型 Critic ),显存占用大, Critic 的估计偏差还可能干扰 advantage 计算。 GRPO 只需策略模型,对每个 prompt 采样 G 个输出,用组内平均奖励作为 baseline 替代 Critic ,去掉了价值模型的全部开销。(2) Baseline 设计: PPO 的 baseline 是 Critic 预测的状态价值; GRPO 的 baseline 是当前 prompt 下同组 G 个 rollout 的均值。(3)适用场景: GRPO 对"组内多样性"要求高,特别适合有可验证奖励的推理任务; PPO 更通用但更复杂,适合需要精确价值估计的场景。4️⃣大模型训练流程。(1)预训练( Pre - training ):目标是从海量无标注文本(万亿 token 级别)学习语言统计规律和世界知识,任务是 next - token prediction 。(2)监督微调( SFT ):用高质量( instruction , response )对让模型学会遵循指令。(3)对齐训练( RLHF / DPO / GRPO ):让模型输出符合人类偏好,通过奖励模型反馈或直接偏好优化进一步提升质量和安全性。5️⃣微调大模型如何卡阈值。可验证任务(数学/代码)用"正确性"作为硬阈值(只要正确的);生成任务用 RM 综合分数阈值。6️⃣为什么 CLIP 的嵌入效果不好?①文本编码器仅支持77 tokens (基于 GPT -2架构),无法处理长文本;② nfoNCE 对比目标只要求"匹配图文对靠近",不需要区分文本之间的细粒度语义差异,嵌入空间对文本相似度分辨能力弱;③预训练数据以互联网短标题为主,语义噪声大,文本侧质量不足;④对文本扰动敏感(微小改动可能导致检索排序大变)。7️⃣[代码题]手撕了 InfoNCE 代码 InfoNCE loss 的实现﹣﹣计算相似度矩阵(点积/余弦)、温度缩放、对角线为正样本的 cross - entropy loss ,批次内负样本。📳对于想求职算法岗的同学,如果想参加高质量项目辅导,提升面试能力,欢迎后台联系。
查看7道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务