欢迎光临
我们一直在努力

Spring 单例 Bean

一、Spring 单例与设计模式单例的核心差异

单例的核心诉求是“实例唯一性”,但 Spring 单例与设计模式(GoF)中的单例,在“唯一性范围”和“实现机制”上存在显著区别:

1.1 设计模式中的单例

定义:一个类在整个 JVM 进程中只有一个实例,严格限制实例的创建逻辑。

实现特征:通过“私有构造器 + 静态方法”强制控制实例创建(如 private Singleton() {} 禁止外部 new 实例,通过 getInstance() 静态方法获取唯一实例);实例生命周期完全由类自身管理,不受任何容器控制。

1.2 Spring 中的单例

定义:一个 Bean 在当前 Spring 容器(ApplicationContext)中只有一个实例,实例唯一性范围是“容器级”而非“JVM 级”。

实现特征:由 Spring 容器负责实例的创建、管理和复用;不限制类的构造器访问权限(构造器可 public),实例可通过 @Autowired 等方式注入到其他 Bean 中;若系统中有多个 Spring 容器(如多个 ApplicationContext 实例),则每个容器可拥有该 Bean 的独立实例。

二、Spring 单例 Bean 的线程安全性:不一定安全

单例 Bean 的实例在多线程间共享,其线程安全性取决于 Bean 的“状态设计”,核心规则:无状态则安全,有状态则可能不安全。

2.1 线程安全场景

若单例 Bean 是“无状态”的(即无成员变量,或成员变量是不可变的,如工具类 StringUtils),则线程安全。因为多线程并发调用时,不会修改共享数据,仅执行方法逻辑,无数据竞争问题。

2.2 线程不安全场景

若单例 Bean 是“有状态”的(包含可修改的成员变量,如 UserService 中有 int count 记录接口调用次数),多线程并发修改成员变量时,会导致数据不一致(线程不安全)。

2.3 线程安全解决方案

  • 避免在单例 Bean 中定义可变成员变量:优先设计无状态 Bean(如将状态数据存入数据库、Redis 等外部存储,而非 Bean 内部);

  • 用 ThreadLocal 存储线程私有数据:ThreadLocal 可确保每个线程拥有独立的变量副本,避免多线程共享;

  • 关键操作加锁:对修改共享数据的核心逻辑,通过 synchronized 关键字或 Lock 接口加锁,保证并发安全。

三、单例 Bean 的创建时机:默认预初始化,支持延迟控制

单例 Bean 的创建时机由“容器初始化策略”和“延迟初始化配置”共同决定,核心规则:默认容器启动时创建,可通过配置改为首次使用时创建。

3.1 默认创建时机

Spring 容器(ApplicationContext)启动时(初始化阶段的 finishBeanFactoryInitialization() 方法),会预初始化所有非延迟的单例 Bean。优势是“提前发现初始化错误”,避免运行时故障;劣势是增加容器启动时间,占用内存。

注意:BeanFactory 容器默认采用“延迟初始化”策略,单例 Bean 需手动调用 getBean() 才会创建。

3.2 延迟初始化控制

通过配置让单例 Bean 延迟创建,即首次被使用时(如 getBean() 调用或被其他 Bean 注入时)才创建,而非容器启动时。

实现方式:

  • 注解方式:在 Bean 类或 @Bean 方法上添加 @Lazy 注解;

  • XML 方式:在 <bean> 标签中配置 lazy-init="true"。

四、单例 Bean 循环依赖的解决方案:三级缓存

循环依赖是指两个或多个 Bean 互相依赖(如 A 依赖 B,B 依赖 A)。Spring 通过“三级缓存”机制解决单例 Bean 的循环依赖,而原型 Bean 的循环依赖无法解决。

4.1 三级缓存的核心定义

Spring 容器通过三个缓存(Map 结构)存储不同状态的单例 Bean,协同解决循环依赖:

  • 一级缓存(singletonObjects):存储“完全初始化完成”的单例 Bean(实例化、属性注入、初始化均完成),供全局复用;

  • 二级缓存(earlySingletonObjects):存储“提前暴露的半成品 Bean”(已实例化,但未完成属性注入和初始化);

  • 三级缓存(singletonFactories):存储“单例 Bean 工厂对象”(ObjectFactory),用于延迟生成半成品 Bean 的早期引用。

4.2 单例循环依赖的解决流程(以 A、B 互相依赖为例)

  • Spring 容器初始化 A Bean:调用 createBean() 完成 A 的实例化(未注入属性、未初始化),随后将 A 的工厂对象(ObjectFactory)存入三级缓存;

  • 为 A 注入依赖 B:容器查找 B 发现未创建,开始初始化 B;

  • 初始化 B Bean:完成 B 的实例化,将 B 的工厂对象存入三级缓存;

  • 为 B 注入依赖 A:容器依次查询一级、二级缓存,均未找到完全初始化的 A;随后查询三级缓存,通过 A 的工厂对象获取 A 的半成品引用(已实例化未初始化);

  • 将 A 的半成品引用从三级缓存移除,存入二级缓存;将 A 的半成品注入到 B 中;

  • B 完成属性注入和初始化,存入一级缓存;

  • 回到 A 的属性注入流程:从一级缓存获取已完全初始化的 B,注入到 A 中;

  • A 完成属性注入和初始化,存入一级缓存;循环依赖解决。

  • 4.3 原型 Bean 循环依赖无法解决的原因

    原型 Bean 的核心特征是“每次请求(getBean())都会创建新实例”,Spring 不会将原型 Bean 存入三级缓存(缓存无意义,需返回新实例)。

    循环依赖时的问题:创建 A 需先创建 B,创建 B 又需先创建 A,陷入“创建 A→创建 B→创建 A→创建 B…”的无限循环,最终容器抛出 BeanCurrentlyInCreationException 异常。

    五、关键疑问:为何三级缓存存储工厂而非直接存储对象?

    三级缓存选择存储 ObjectFactory(工厂)而非直接存储半成品 Bean,核心目的是“支持 AOP 代理的延迟生成”和“避免不必要的实例化”,具体原因:

    5.1 核心目的 1:支持 AOP 代理的延迟生成

    循环依赖场景下,若 Bean A 需要被 AOP 增强(生成代理对象),则不能提前暴露原始的半成品 Bean——否则注入到 B 中的是原始对象,后续 A 生成代理后,B 中引用的 A 与容器最终的代理 A 不一致。

    工厂的作用:通过工厂的 getObject() 方法,在 B 注入 A 时动态生成 A 的代理对象(核心触发逻辑:SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference()),确保注入的是代理对象,与容器最终管理的 Bean 一致。

    5.2 核心目的 2:避免不必要的实例化

    工厂是“懒加载”的,仅当触发循环依赖时(如 B 需注入 A),才会调用 getObject() 生成 A 的半成品实例;若不存在循环依赖,工厂不会被触发,避免提前实例化 Bean 造成的内存浪费。

    总结

    Spring 单例 Bean 的核心注意点:① 与设计模式单例的差异在于“容器级唯一” vs “JVM 级唯一”;② 线程安全性取决于是否有状态,需通过无状态设计、ThreadLocal 或加锁保障;③ 创建时机默认容器启动时预初始化,可通过 @Lazy 延迟;④ 单例循环依赖通过三级缓存解决,原型循环依赖因无缓存支持无法解决;⑤ 三级缓存存储工厂是为了支持 AOP 代理延迟生成和避免不必要实例化。理解这些机制,才能合理使用单例 Bean,规避潜在问题。

    赞(0)
    未经允许不得转载:171主机测评 » Spring 单例 Bean
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址