Java 框架 Spring Bean 加载、创建、管理层面线程安全吗
核心结论:
Spring IoC 容器本身在 Bean 的加载、创建、和管理层面是线程安全的。它的线程安全是通过精心的设计和底层同步机制保证的。然而,线程安全的终极责任落在了我们开发者编写的 Bean 本身的行为上。
让我们从容器和业务两个层面来分解这个问题。
一、容器层面:Spring 如何保证自身流程的线程安全
Spring 的线程安全核心在于其容器的数据结构和对这些结构的访问控制。我们重点关注负责单例 Bean 管理的 DefaultSingletonBeanRegistry。
1. 三级缓存与同步控制
Bean 的创建中心——三级缓存,是解决循环依赖的关键,也是保证并发创建不会出问题的核心。
public class DefaultSingletonBeanRegistry ... {
// 一级缓存:存放完全初始化好的单例 Bean (成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存放早期暴露的 Bean 引用 (半成品),用于解决循环依赖
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:存放 ObjectFactory,用于生成早期引用,是解决 AOP 代理与循环依赖的关键
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// ★ 关键:所有对上述缓存进行“写操作”的方法都使用 synchronized 同步!
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) { // 以一级缓存为锁进行同步
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
}
}
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) { // 同样以一级缓存为锁
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
}
}
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 首先无锁检查一级缓存(快速路径)
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 如果未初始化完成且在创建中,则进入同步块
synchronized (this.singletonObjects) { // 同样以一级缓存为锁
// 检查二级缓存
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 检查三级缓存
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 从三级缓存的 ObjectFactory 中获取早期引用
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
}
流程与线程安全保证:
- 并发
getBean():当多个线程同时请求一个尚未创建的单例 Bean 时,都会进入到doCreateBean流程。 - 同步点:它们都会执行到
addSingletonFactory,将创建 Bean 的ObjectFactory加入到三级缓存。这个方法是synchronized的,所以多个线程会串行执行,确保三级缓存的数据不会错乱。 - “第一个”线程:成功将
ObjectFactory加入三级缓存的线程继续执行后续的 populate 和 initialize。 - “后续”线程:在执行到
getSingleton(beanName, true)尝试解决依赖时,会因为第一个线程已经将ObjectFactory放入三级缓存,从而能够通过getEarlyBeanReference()获取到早期引用(可能是原始对象或代理对象)。这个早期引用会被放入二级缓存,供所有后续线程使用。 - 最终完成:第一个线程完成初始化后,调用
addSingleton方法(也是synchronized的),将完整 Bean 从二级/三级缓存升级到一级缓存。 - 其他线程获取:之后所有线程再调用
getSingleton时,直接从一级缓存拿到完整的 Bean 返回。
Mermaid 序列图:多线程获取单例 Bean
通过这种以 singletonObjects 为监视器的同步块和三级缓存机制,Spring 确保了:
- 一个单例 Bean 只会被创建一次。
- 在 Bean 创建过程中,其他线程不会无限等待,而是可以拿到一个“早期引用”先行使用,从而提升并发效率(避免了在整个
doCreateBean方法上加锁)。 - 缓存状态的一致性。
二、业务层面:开发者的责任(线程安全的真正风险点)
容器保证了 Bean 的创建和管理是安全的,但如果 Bean 本身的状态和行为不是线程安全的,那么并发访问时就会出问题。这是绝大多数并发问题的根源。
1. 非线程安全的 Bean (有状态 Bean)
如果你的 Bean 包含了可变的成员变量,并且有方法会修改这个状态,那么它就是非线程安全的。
@Service
public class NotThreadSafeService {
private int count = 0; // ★ 可变状态
public void increment() {
count++; // ★ 非原子操作,多线程下会出问题
}
public int getCount() {
return count;
}
}
- 场景:多个线程同时调用
increment()方法,count的最终值会小于实际调用次数,因为count++不是原子操作。 - 结论:Spring 不会也不能为你保证业务状态的线程安全。
2. 线程安全的 Bean (无状态或使用同步)
-
无状态 Bean (最佳实践):这是最推荐的方式。Bean 不包含任何可变的成员变量,它的行为仅由参数和内部逻辑决定。
@Service public class StatelessService { // ★ 没有成员变量,线程安全 public String process(String input) { return "Processed: " + input; } }- 像
@Controller,@Service,@Repository注解的类,通常都应该设计成无状态的。
- 像
-
使用同步机制:如果必须有状态,开发者必须自行保证线程安全。
- 使用
synchronized关键字 - 使用
java.util.concurrent包下的线程安全类 (如AtomicInteger,ConcurrentHashMap)
@Service public class ThreadSafeService { private final AtomicInteger count = new AtomicInteger(0); // ★ 使用原子类 public void increment() { count.incrementAndGet(); // ★ 原子操作,线程安全 } } - 使用
3. Prototype(原型)Scope 的 Bean
- Spring 每次请求都会创建一个新的 Prototype Bean 实例。
- 由于每个线程获取到的是不同的实例,所以不存在共享资源竞争的问题,自然是线程安全的。
- 线程安全问题转移到了 Prototype Bean 内部本身(如果它内部操作了静态变量或共享外部资源)。
三、总结与最佳实践
| 问题点 | 是否线程安全? | 原因与解决方案 |
|---|---|---|
| Spring 单例池 (IoC 容器) | 是 | 通过 synchronized 块和三级缓存机制保证。 |
| 无状态单例 Bean | 是 | Bean 没有可变状态,是线程安全的。这是推荐的做法。 |
| 有状态单例 Bean | 否 | Bean 包含可变状态,需开发者使用同步机制(如synchronized, Lock, 原子类)来保证线程安全。 |
| Prototype/Request/Session Scope Bean | 是 (相对于容器) | 每个线程/请求持有自己的实例,不存在共享。但Bean内部逻辑仍需自己保证安全。 |
最终答案:
Bean 的加载和创建过程本身是线程安全的,这是由 Spring Framework 通过同步机制保证的。但是,Bean 被加载到容器中之后,在并发环境下被访问时是否安全,完全取决于 Bean 本身的实现。
给你的建议:
- 极力推崇无状态设计:这是保证线程安全最简单、最有效的方式。
- 明确区分状态:如果必须使用状态,要非常清楚哪些状态是共享的,哪些是线程隔离的。
- 优先使用并发工具:而不是自己实现复杂的同步逻辑,优先考虑
java.util.concurrent.*包下的类。 - 谨慎使用
@Scope("prototype"):不要把它当作解决线程安全问题的银弹,因为它会带来更大的创建开销和内存消耗。只有在真正需要每次都创建新实例时才使用它(例如,Bean 本身持有非线程安全的状态且该状态在每次使用后都会变化)。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120819

浙公网安备 33010602011771号