面试题:为什么 Spring 循环依赖需要三级缓存,二级不够吗?

Spring 使用三级缓存来解决循环依赖问题,而不是仅仅使用二级缓存。这是因为三级缓存的设计是为了解决更复杂的场景,并确保 Bean 的正确性和一致性。以下是详细原因:


1. 三级缓存的作用

Spring 的三级缓存分别是:

  1. Singleton Objects Cache(一级缓存)
    存放完全初始化好的单例 Bean。
  2. Early Singleton Objects Cache(二级缓存)
    存放提前暴露的 Bean(半成品 Bean,尚未完成属性注入和初始化)。
  3. Singleton Factories Cache(三级缓存)
    存放 Bean 的工厂对象(ObjectFactory),用于创建 Bean 的早期引用。

2. 为什么需要三级缓存?

场景分析

假设只有二级缓存(一级缓存 + 二级缓存),会出现以下问题:

  1. 无法处理代理对象的循环依赖
    Spring 中的 Bean 可能会被代理(例如通过 AOP 或 @Transactional 注解)。代理对象的创建需要在原始对象的基础上生成,而二级缓存无法区分原始对象和代理对象。
  2. 无法保证 Bean 的一致性
    如果只有二级缓存,当多个线程同时请求同一个 Bean 时,可能会导致不一致的问题。三级缓存通过工厂对象(ObjectFactory)动态创建 Bean 的早期引用,确保每次获取的都是同一个实例。

三级缓存的优势

  1. 支持代理对象的创建
    三级缓存中的 ObjectFactory 可以在需要时动态生成代理对象,确保代理对象的正确性。
  2. 确保 Bean 的一致性
    通过工厂对象,Spring 可以确保在多个线程同时请求同一个 Bean 时,返回的是同一个实例。
  3. 延迟创建 Bean 的早期引用
    三级缓存允许 Spring 在需要时才创建 Bean 的早期引用,而不是在 Bean 实例化后立即放入二级缓存。这样可以避免不必要的对象创建和内存占用。

3. 三级缓存的工作流程

以下是一个典型的循环依赖场景:

  • BeanA 依赖于 BeanBBeanB 又依赖于 BeanA

解决步骤:

  1. 创建 BeanA
    • Spring 调用 BeanA 的构造函数,生成一个半成品对象。
    • 将 BeanA 的工厂对象(ObjectFactory)放入三级缓存
  2. 注入 BeanA 的依赖
    • Spring 发现 BeanA 依赖于 BeanB,于是开始创建 BeanB
  3. 创建 BeanB
    • Spring 调用 BeanB 的构造函数,生成一个半成品对象。
    • 将 BeanB 的工厂对象(ObjectFactory)放入三级缓存
  4. 注入 BeanB 的依赖
    • Spring 发现 BeanB 依赖于 BeanA,于是尝试从缓存中获取 BeanA
    • 三级缓存中获取 BeanA 的工厂对象,调用 getObject() 方法生成 BeanA 的早期引用(半成品)。
    • 将 BeanA 的早期引用放入二级缓存,并从三级缓存中移除 BeanA 的工厂对象。
  5. 完成 BeanB 的初始化
    • Spring 将 BeanA 的早期引用注入到 BeanB 中,完成 BeanB 的属性注入和初始化。
    • 将完全初始化的 BeanB 放入一级缓存
  6. 完成 BeanA 的初始化
    • Spring 将完全初始化的 BeanB 注入到 BeanA 中,完成 BeanA 的属性注入和初始化。
    • 将完全初始化的 BeanA 放入一级缓存

4. 为什么二级缓存不够?

如果只有二级缓存,以下问题无法解决:

  1. 代理对象的创建问题
    二级缓存无法区分原始对象和代理对象,导致代理对象的创建无法正确完成。
  2. 多线程一致性问题
    二级缓存无法保证在多个线程同时请求同一个 Bean 时的一致性。
  3. 内存占用问题
    二级缓存需要提前将半成品 Bean 放入缓存,而三级缓存通过工厂对象延迟创建早期引用,减少内存占用。

5. 总结

Spring 使用三级缓存来解决循环依赖问题,主要原因包括:

  • 支持代理对象的创建。
  • 确保 Bean 的一致性。
  • 延迟创建 Bean 的早期引用,减少内存占用。
THE END
点赞14 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容