Spring 中的 Bean 是线程安全的吗?


面试考察点

  1. 作用域理解:面试官不仅仅是想知道 "安全还是不安全" 这个二选一的答案,更想考察你是否清楚 Spring Bean 的不同作用域(SingletonPrototype 等),以及作用域如何影响线程安全。

  2. 线程安全意识:考察你是否理解多线程环境下共享变量的风险,能不能区分 "有状态" 和 "无状态" Bean,以及在实际开发中如何规避线程安全问题。

  3. 框架原理深度:如果你能说出 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 包装需要线程内共享状态的场景
同步机制synchronizedReentrantLock必须共享且需要原子操作

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 的 RequestContextHolderLocaleContextHolder 内部就是用 ThreadLocal 实现的。

不过要注意:ThreadLocal 用完必须调用 .remove() 清理,特别是在线程池环境下,线程会被复用,不清理会导致脏数据和内存泄漏。这个坑我之前做项目的时候真的踩过,排查了好久才发现是 ThreadLocal 没清理。

四、常见误区

误区一:"Spring Bean 都不是线程安全的"

太绝对了。无状态的 Singleton Bean 本身就是线程安全的。问题不在 Spring,在于你是否在 Bean 里写了可变的成员变量。

误区二:"改成 Prototype 就万事大吉了"

Prototype 确实每次创建新实例,但它带来了更多的对象创建和 GC 开销。而且如果 Prototype Bean 内部注入了 Singleton Bean,共享的依赖部分依然可能存在线程安全问题。不要滥用,无状态设计才是正道。

误区三:"@Controller 是线程安全的因为每个请求独立"

错。@Controller 默认也是 Singleton,一个 Controller 实例处理所有请求。如果在 Controller 里定义了可变的成员变量,一样会出问题。

面试高频追问

  1. 追问一:Spring 的 @Controller 是线程安全的吗?

    不是。@Controller 默认是 Singleton 作用域,多线程共享同一个实例。如果在 Controller 里定义了可变的成员变量(比如用成员变量存储请求参数),就存在线程安全问题。正确做法是使用方法参数接收请求参数,不要用成员变量。

  2. 追问二:ThreadLocal 有什么坑?

    两个大坑:一是内存泄漏(ThreadLocalEntry 是弱引用,但 value 是强引用,线程池复用时 value 无法回收),二是数据脏读(线程池中线程被复用,上一次请求的数据残留)。解决方案就是用完必须 remove()

  3. 追问三:Spring 有没有线程安全的 Bean 作用域?

    request 作用域(@Scope("request"))是线程安全的,因为每个 HTTP 请求创建一个实例,单线程内使用。但只适用于 Web 环境。

常见面试变体

  • "Spring @Service 是线程安全的吗?"
  • "Spring Bean 的成员变量会有线程安全问题吗?怎么解决?"
  • "SingletonPrototype 作用域有什么区别?"
  • "如何保证 Spring Bean 的线程安全?"

记忆口诀

Spring Bean 线程安全三要素:看作用域、看有没有状态、无状态最靠谱。

Singleton + 有状态 = 必出事;Singleton + 无状态 = 没问题。

总结

Spring Bean 默认是 Singleton 作用域,容器不保证线程安全。但只要你遵循 无状态设计——不在 Bean 中定义可变的成员变量,只通过方法参数和局部变量处理业务逻辑,线程安全就不是问题。面试时记住一句话就够了:Spring 不管线程安全,你自己管——最佳实践就是写无状态的 Bean。