LinkedBear
LinkedBear
Published on 2025-06-04 / 26 Visits
0
0

Spring中的单实例Bean是线程安全的吗?

近日小伙伴给我推了一个知乎的问题,问题就一句话:Spring中的单实例Bean是线程安全的吗?这一下子就提起了我的论述之心。本着要讲就讲透的原则,这篇文章就来详细论述一下该问题。

原问题的题主的顾虑点在 Bean 的延迟加载,实际上单纯论 Bean 的线程安全问题而言,安全不安全取决于管理和使用该 Bean 的方式,而不是这个 Bean 本身是否具备线程安全问题

我们可以先回顾一下线程安全问题的常见场景。

常见的线程安全问题

有状态的服务类

@Service
public class OrderService {

    private List<Order> recentOrders = new ArrayList<>(); 

    public void addOrder(Order newOrder) {
        // ArrayList本身线程不安全,多个线程并发调用addOrder也会引发线程安全问题
        recentOrders.add(newOrder);
    }

    public List<Order> getRecentOrders() {
        // 线程获取recentOrders集合时,如果recentOrders正在被修改,则会抛出异常
        return recentOrders;
    }
}

内部共享的成员变量

@Service
public class ReportService {

    @Autowired
    private DataLoader dataLoader;

    @Autowired
    private ReportWriter reportWriter;

    // 使用内部可共享的成员变量,埋雷
    private Report currentReport; 

    public void writeReport(Long reportId) {
        // 获取数据的耗时不稳定,不同的报表源数据获取时耗时可能天差地别
        List<Map<String, Object>> data = dataLoader.queryByReportId(reportId);
        // 此处使用了成员变量,这也就是引发线程问题的根源
        currentReport = new Report(reportId);
        // 这一步用来模拟一个很耗时的数据处理,而且可能会改动currentReport内部的数据
        processData(data, currentReport);
        reportWriter.write(currentReport);
    }

    private void processData(Data data, Report report) { ... }
}

发现问题了吗?Bean 的线程安全问题都是因为内部的设计导致的,无论是使 Bean 设计为有状态,还是内部定义了可共享的成员变量,都是因为 Bean 本身的设计有缺陷,而不是别人使用该 Bean 的时候出现了线程安全问题,因此我们可以先得出一个初步结论:

单实例 Bean 是否有线程安全问题,需要先确定 Bean 本身的设计是否存在线程安全问题。如果本身设计有问题,则大概率有线程安全问题。


有了上面的初步论述,接下来我们试着从不同的角度分析,如果单实例 Bean 本身的设计没有线程安全问题,那么在 IOC 容器中这些单实例 Bean 是否还有线程安全问题。

IOC容器初始化阶段

默认情况下,非延迟加载的单实例 Bean 应当在 IOC 容器的初始化阶段(也就是 ApplicationContextrefresh 阶段)就被统一创建,而这个阶段的处理通常由程序的主线程完成(当然不排除特定情况下创建 IOC 容器会使用专用线程,本回答暂不考虑),并且应用程序通常不会向外提供服务,因此该阶段一般不会出现并发问题。

初始化单实例 Bean 的过程本身就是串行化的(单线程),那么这个环节中 IOC 容器本身不会引发线程安全问题。

又由于单实例 Bean 本身设计没有问题,因此下结论:IOC 容器初始化阶段不会出现线程安全问题

单实例Bean的延迟加载阶段(@Lazy注解)

如果一个单实例 Bean 被标记为延迟加载(即被标注了 @Lazy 注解),那么通常情况下它就不会参与 IOC 容器统一创建单实例 Bean 的环节,而是当第一次被请求使用时才开始创建。

这个“第一次被请求”有些抽象,简单来说就是第一次调用ApplicationContext的getBean方法获取这个 Bean ,或者一些标注了 @Autowired 等依赖注入的注解注入这个 Bean 的地方第一次要使用这个 Bean 了,就触发了“第一次被请求”的时机。

题主纠结的地方就在这里,延迟加载的 Bean 会不会引发线程安全问题?其实是不会的,IOC 容器在底层设计中,创建每个单实例 Bean 时都会引入双检锁保证创建 Bean 的过程是线程安全的,因此即便有多个线程都抢着获取这个延迟 Bean ,IOC 容器也会保证只创建出一个 bean 对象

创建出 bean 对象后,剩下要考虑的就跟上一小节一样了,只要 Bean 的设计本身没有问题,那么:单实例Bean的延迟加载阶段也不会出现线程安全问题

有关更详细的 IOC 容器创建 Bean 的流程和讲解,可以参考我著作的图书《SpringBoot源码解读与原理分析》7.11.4节,里面有非常细致入微的流程讲解,可以帮助你解除该环节的困惑和顾虑。

应用程序运行期

当应用进入运行期后,此时应用正在对外提供服务,IOC 容器中只会保存一个该 Bean 的对象,出现多线程访问该对象的场景大多是客户端请求打到同一个 Controller ,或者好多个 Service 注入了同一个 Mapper 接口等情况。当多个请求同时访问同一个 bean 对象时,那么是否引发线程安全问题就只剩下一个决定性因素了:Bean 本身的设计是否是线程安全的

上面我们已经讨论过,Bean 本身的设计没有问题(即无状态 Bean),那么使用过程就不会出现线程安全问题,反之有状态 Bean 就有可能出现线程安全问题

因此下结论:本身设计没问题的 Bean 在应用程序运行期也不会出现线程安全问题


综上所述,给出最终结论:本身设计没有线程安全问题的单实例 Bean 是线程安全的。


Comment