Spring 中的 Bean 是线程安全的吗?
面试考察点
-
作用域理解:面试官不仅仅是想知道 "安全还是不安全" 这个二选一的答案,更想考察你是否清楚 Spring Bean 的不同作用域(
Singleton、Prototype等),以及作用域如何影响线程安全。 -
线程安全意识:考察你是否理解多线程环境下共享变量的风险,能不能区分 "有状态" 和 "无状态" Bean,以及在实际开发中如何规避线程安全问题。
-
框架原理深度:如果你能说出 Spring 并没有对 Bean 做线程安全的保障,而是把责任交给了开发者,说明你对 Spring 容器的定位理解到位。
核心答案
直接说结论:Spring 中的 Bean 默认不是线程安全的。
准确地说,Spring 容器本身并不提供 Bean 的线程安全保证。线程安全与否,取决于 Bean 的 作用域 和 是否有状态。
| 作用域 | 说明 | 线程安全性 |
|---|---|---|
Singleton(默认) | 整个容器只有一个实例 | ⚠️ 不安全(多线程共享) |
Prototype | 每次获取都创建新实例 | ✅ 相对安全(各线程独立) |
Request | 每个 HTTP 请求一个实例 | ✅ 安全(单线程内使用) |
Session | 每个 HTTP Session 一个实例 | ⚠️ 可能不安全(同 Session 多请求并发) |
绝大多数 Bean 都是 Singleton 作用域,多个线程会同时访问同一个实例,如果这个 Bean 里有可变的成员变量,那就是线程不安全的。
深度解析
一、为什么 Singleton Bean 不安全?
先看一个踩坑示例:
@Component
public class UserService {
// 危险!这是可变的成员变量
private String currentUser;
public void processUser(String username) {
this.currentUser = username; // 线程 A 设置了 "张三"
// 线程 B 可能在此时把 currentUser 改成了 "李四"
doSomething(); // 线程 A 读到的可能是 "李四"!
}
}
上图清楚地展示了问题所在:Singleton 作用域下,Spring 容器里只有一个 UserService 实例,所有线程共享它。一旦某个线程修改了成员变量 currentUser,其他线程读到的就是脏数据。
- 根因:多个线程同时操作同一个对象的可变成员变量,没有任何同步机制保护
- 表现:数据错乱、脏读、甚至
NullPointerException(对象被部分修改)
二、那为什么我们平时写的 Bean 没出问题?
因为实际开发中,我们写的 Bean 大多是 无状态 的。
@Component
public class UserService {
@Autowired
private UserMapper userMapper; // 注入的依赖,本身也是无状态的
public User getUserById(Long id) {
// 没有可变的成员变量,只有局部变量和参数
// 局部变量存在线程私有的栈中,天然线程安全
return userMapper.selectById(id);
}
}
这里的关键是:
- 无状态 Bean:没有可变的成员变量,所有数据都通过方法参数传递,使用局部变量处理。这种 Bean 天然线程安全
- 有状态 Bean:包含可变的成员变量(如计数器、用户信息缓存等),多线程并发访问时就有问题
说实话,Spring 鼓励的编程模型本身就是无状态的——@Controller、@Service、@Repository 这些组件,大多数情况下只注入其他 Bean 作为依赖,不维护可变状态。所以日常开发中线程安全问题并不常见,但这不等于 "Spring Bean 是线程安全的"。
三、如何解决线程安全问题?
| 方案 | 做法 | 适用场景 |
|---|---|---|
| 无状态设计(推荐) | 不使用可变成员变量,全部用方法参数和局部变量 | 大多数业务场景 |
| 改为 Prototype 作用域 | @Scope("prototype"),每次获取新实例 | 少数需要状态的场景 |
| 使用 ThreadLocal | 成员变量用 ThreadLocal 包装 | 需要线程内共享状态的场景 |
| 同步机制 | synchronized 或 ReentrantLock | 必须共享且需要原子操作 |
ThreadLocal 方案的示例:
@Component
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public void setUser(User user) {
currentUser.set(user);
}
public User getUser() {
return currentUser.get();
}
public void clear() {
currentUser.remove(); // 用完必须清理,防止内存泄漏!
}
}
ThreadLocal 为每个线程维护独立的变量副本,线程之间互不干扰。Spring 的 RequestContextHolder、LocaleContextHolder 内部就是用 ThreadLocal 实现的。
不过要注意:ThreadLocal 用完必须调用 .remove() 清理,特别是在线程池环境下,线程会被复用,不清理会导致脏数据和内存泄漏。这个坑我之前做项目的时候真的踩过,排查了好久才发现是 ThreadLocal 没清理。
四、常见误区
误区一:"Spring Bean 都不是线程安全的"
太绝对了。无状态的 Singleton Bean 本身就是线程安全的。问题不在 Spring,在于你是否在 Bean 里写了可变的成员变量。
误区二:"改成 Prototype 就万事大吉了"
Prototype 确实每次创建新实例,但它带来了更多的对象创建和 GC 开销。而且如果 Prototype Bean 内部注入了 Singleton Bean,共享的依赖部分依然可能存在线程安全问题。不要滥用,无状态设计才是正道。
误区三:"@Controller 是线程安全的因为每个请求独立"
错。@Controller 默认也是 Singleton,一个 Controller 实例处理所有请求。如果在 Controller 里定义了可变的成员变量,一样会出问题。
面试高频追问
-
追问一:Spring 的
@Controller是线程安全的吗?不是。
@Controller默认是Singleton作用域,多线程共享同一个实例。如果在 Controller 里定义了可变的成员变量(比如用成员变量存储请求参数),就存在线程安全问题。正确做法是使用方法参数接收请求参数,不要用成员变量。 -
追问二:
ThreadLocal有什么坑?两个大坑:一是内存泄漏(
ThreadLocal的Entry是弱引用,但value是强引用,线程池复用时value无法回收),二是数据脏读(线程池中线程被复用,上一次请求的数据残留)。解决方案就是用完必须remove()。 -
追问三:Spring 有没有线程安全的
Bean作用域?request作用域(@Scope("request"))是线程安全的,因为每个 HTTP 请求创建一个实例,单线程内使用。但只适用于 Web 环境。
常见面试变体
- "Spring
@Service是线程安全的吗?" - "Spring Bean 的成员变量会有线程安全问题吗?怎么解决?"
- "
Singleton和Prototype作用域有什么区别?" - "如何保证 Spring Bean 的线程安全?"
记忆口诀
Spring Bean 线程安全三要素:看作用域、看有没有状态、无状态最靠谱。
Singleton + 有状态 = 必出事;Singleton + 无状态 = 没问题。
总结
Spring Bean 默认是 Singleton 作用域,容器不保证线程安全。但只要你遵循 无状态设计——不在 Bean 中定义可变的成员变量,只通过方法参数和局部变量处理业务逻辑,线程安全就不是问题。面试时记住一句话就够了:Spring 不管线程安全,你自己管——最佳实践就是写无状态的 Bean。
