Spring Bean 作用域、线程安全与生命周期

面试里问"Spring 中的单例 Bean 是线程安全的吗",真正考的不是背一句"不是线程安全",而是你能不能把 Bean 的作用域、对象状态、Spring 容器创建过程 串起来。

一句话先给结论:Spring 默认把 Bean 做成单例,但单例只代表容器里只有一个对象,不代表这个对象一定线程安全。是否线程安全,取决于这个 Bean 里面有没有被多个线程共享并修改的状态。


没有

Spring 容器创建单例 Bean
多个请求线程复用同一个 Bean
Bean 内是否有可变成员变量
无状态 Service 或 DAO
通常没有线程安全问题
多个线程共享修改同一份状态
需要改局部变量、加锁或调整作用域

Bean 默认是不是单例

Spring 常见的 Bean 作用域里,最常用的是 singletonprototype

作用域 含义 常见场景
singleton 一个 Spring IoC 容器中只有一个 Bean 实例 Controller、Service、DAO
prototype 每次获取 Bean 都创建一个新实例 有独立状态、短生命周期对象

默认情况下,Spring 管理的 Bean 是 singleton

java 复制代码
@Service
@Scope("singleton")
public class UserServiceImpl implements UserService {
}

这段代码里的 UserServiceImpl 在同一个 Spring 容器里只会有一个实例。多个请求进来时,访问的是同一个 Service 对象。

单例为什么不等于线程安全

Web 项目里,一个请求通常由一个线程处理。假设 Controller 里放了一个可变成员变量:

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    private int count;

    @Autowired
    private UserService userService;

    @GetMapping("/getById/{id}")
    public User getById(@PathVariable Integer id) {
        count++;
        System.out.println(count);
        return userService.getById(id);
    }
}

count 是 Controller 对象的成员变量,而 Controller 默认是单例。多个请求线程同时执行 count++ 时,就会竞争同一个变量。

这就是线程不安全的来源:可变成员变量被多个线程共享修改

反过来,平时我们写的 Service 和 DAO 大多是无状态的:

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User getById(Integer id) {
        return userMapper.selectById(id);
    }
}

这里没有在 Bean 自己身上保存请求级数据。id 是方法局部变量,每个线程有自己的栈帧,所以没有共享状态。这样的单例 Bean 在通常业务下可以认为是线程安全的。

真的需要状态怎么办

如果 Bean 中确实有会被修改的成员变量,常见处理方式有四种:

方案 适合场景 注意点
改成局部变量 请求临时数据、计算中间值 最推荐,简单稳定
使用无状态设计 Service、DAO、Controller 后端业务类的默认选择
加锁或使用并发容器 全局计数器、共享缓存 要评估性能和锁粒度
改成 prototype 对象必须携带独立状态 Web 层并不常用,注入方式也要注意

面试回答时,不要只说"用多例解决"。大多数业务类更好的方案是:不要把请求状态放到单例 Bean 的成员变量里

Bean 生命周期怎么走

Spring 创建 Bean 并不是 new 一个对象这么简单。容器会先把配置或注解解析成 BeanDefinition,再根据定义信息完成实例化、依赖注入、初始化、代理增强和销毁。


解析配置和注解
生成

BeanDefinition
调用构造方法

实例化 Bean
依赖注入

完成属性赋值
处理 Aware

接口回调
BeanPostProcessor

前置处理
执行初始化方法
BeanPostProcessor

后置处理
可能生成

AOP 代理
容器关闭时

销毁 Bean

可以把生命周期拆成 8 步:

  1. 读取配置或注解,生成 BeanDefinition
  2. 调用构造方法实例化 Bean。
  3. 给 Bean 做依赖注入,也就是给属性赋值。
  4. 处理 Aware 接口,例如 BeanNameAwareBeanFactoryAwareApplicationContextAware
  5. 执行 BeanPostProcessor 的前置处理。
  6. 执行初始化方法,例如 InitializingBean#afterPropertiesSet 或自定义 init-method
  7. 执行 BeanPostProcessor 的后置处理,这一步可能产生 AOP 代理对象。
  8. 容器关闭时销毁 Bean。

BeanDefinition 可以理解成 Bean 的"图纸"。里面会记录类名、作用域、是否懒加载、初始化方法、属性值等信息。

xml 复制代码
<bean id="userService"
      class="com.example.UserServiceImpl"
      scope="singleton"
      lazy-init="true">
    <property name="userDao" ref="userDao"/>
</bean>

Spring 不是直接凭空创建对象,而是先把这些信息封装起来,再按生命周期流程创建 Bean。

面试回答模板

可以这样回答:

Spring 中的 Bean 默认是单例的,也就是同一个 IoC 容器中只有一个实例。但单例 Bean 本身不保证线程安全,关键看 Bean 里有没有可变共享状态。一般 Controller、Service、DAO 都是无状态对象,请求数据放在方法参数和局部变量里,所以通常没有线程安全问题。如果在单例 Bean 中定义了会被多个线程修改的成员变量,就要考虑线程安全,可以改成局部变量、无状态设计、加锁,或者在特殊场景下使用多例。

如果继续问生命周期,可以补一句:

Spring 会先解析配置生成 BeanDefinition,然后实例化 Bean、做依赖注入、处理 Aware 接口、执行 BeanPostProcessor 前置、执行初始化方法、执行 BeanPostProcessor 后置,最后在容器关闭时销毁 Bean。AOP 代理通常发生在后置处理阶段。

小结

Spring Bean 线程安全问题的核心不是 singleton 这个词,而是对象状态。

只要记住这条线就够了:默认单例 -> 多线程共享同一个 Bean -> 无状态通常安全 -> 有可变成员变量就要处理并发问题 -> 生命周期里后置处理可能生成代理对象

相关推荐
小羊没烦恼!1 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
俊昭喜喜里1 天前
java中的继承和多态的区别
java
小羊没烦恼!1 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕1 天前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码1 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖1 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时1 天前
01-06-A-JVM排查实战详解
java·jvm
vipxieliang1 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
ba_pi1 天前
mysql 查询所有表名并授权
java
ProcessOn官方账号1 天前
java函数式编程--入门基础
java·编程·函数式编程