Spring Bean 作用域、线程安全与生命周期
面试里问“Spring 中的单例 Bean 是线程安全的吗”,真正考的不是背一句“不是线程安全”,而是你能不能把 Bean 的作用域、对象状态、Spring 容器创建过程串起来。
一句话先给结论:Spring 默认把 Bean 做成单例,但单例只代表容器里只有一个对象,不代表这个对象一定线程安全。是否线程安全,取决于这个 Bean 里面有没有被多个线程共享并修改的状态。
1. Bean 默认是不是单例
Spring 常见的 Bean 作用域里,最常用的是singleton和prototype。
| 作用域 | 含义 | 常见场景 |
|---|---|---|
| singleton | 一个 Spring IoC 容器中只有一个 Bean 实例 | Controller、Service、DAO |
| prototype | 每次获取 Bean 都创建一个新实例 | 有独立状态、短生命周期对象 |
默认情况下,Spring 管理的 Bean 是singleton。
@Service@Scope("singleton")publicclassUserServiceImplimplementsUserService{}这段代码里的UserServiceImpl在同一个 Spring 容器里只会有一个实例。多个请求进来时,访问的是同一个 Service 对象。
2. 单例为什么不等于线程安全
Web 项目里,一个请求通常由一个线程处理。假设 Controller 里放了一个可变成员变量:
@RestController@RequestMapping("/user")publicclassUserController{privateintcount;@AutowiredprivateUserServiceuserService;@GetMapping("/getById/{id}")publicUsergetById(@PathVariableIntegerid){count++;System.out.println(count);returnuserService.getById(id);}}count是 Controller 对象的成员变量,而 Controller 默认是单例。多个请求线程同时执行count++时,就会竞争同一个变量。
这就是线程不安全的来源:可变成员变量被多个线程共享修改。
反过来,平时我们写的 Service 和 DAO 大多是无状态的:
@ServicepublicclassUserService{@AutowiredprivateUserMapperuserMapper;publicUsergetById(Integerid){returnuserMapper.selectById(id);}}这里没有在 Bean 自己身上保存请求级数据。id是方法局部变量,每个线程有自己的栈帧,所以没有共享状态。这样的单例 Bean 在通常业务下可以认为是线程安全的。
3. 真的需要状态怎么办
如果 Bean 中确实有会被修改的成员变量,常见处理方式有四种:
| 方案 | 适合场景 | 注意点 |
|---|---|---|
| 改成局部变量 | 请求临时数据、计算中间值 | 最推荐,简单稳定 |
| 使用无状态设计 | Service、DAO、Controller | 后端业务类的默认选择 |
| 加锁或使用并发容器 | 全局计数器、共享缓存 | 要评估性能和锁粒度 |
| 改成 prototype | 对象必须携带独立状态 | Web 层并不常用,注入方式也要注意 |
面试回答时,不要只说“用多例解决”。大多数业务类更好的方案是:不要把请求状态放到单例 Bean 的成员变量里。
4. Bean 生命周期怎么走
Spring 创建 Bean 并不是new一个对象这么简单。容器会先把配置或注解解析成BeanDefinition,再根据定义信息完成实例化、依赖注入、初始化、代理增强和销毁。
可以把生命周期拆成 8 步:
- 读取配置或注解,生成
BeanDefinition。 - 调用构造方法实例化 Bean。
- 给 Bean 做依赖注入,也就是给属性赋值。
- 处理 Aware 接口,例如
BeanNameAware、BeanFactoryAware、ApplicationContextAware。 - 执行
BeanPostProcessor的前置处理。 - 执行初始化方法,例如
InitializingBean#afterPropertiesSet或自定义init-method。 - 执行
BeanPostProcessor的后置处理,这一步可能产生 AOP 代理对象。 - 容器关闭时销毁 Bean。
BeanDefinition可以理解成 Bean 的“图纸”。里面会记录类名、作用域、是否懒加载、初始化方法、属性值等信息。
<beanid="userService"class="com.example.UserServiceImpl"scope="singleton"lazy-init="true"><propertyname="userDao"ref="userDao"/></bean>Spring 不是直接凭空创建对象,而是先把这些信息封装起来,再按生命周期流程创建 Bean。
5. 面试回答模板
可以这样回答:
Spring 中的 Bean 默认是单例的,也就是同一个 IoC 容器中只有一个实例。但单例 Bean 本身不保证线程安全,关键看 Bean 里有没有可变共享状态。一般 Controller、Service、DAO 都是无状态对象,请求数据放在方法参数和局部变量里,所以通常没有线程安全问题。如果在单例 Bean 中定义了会被多个线程修改的成员变量,就要考虑线程安全,可以改成局部变量、无状态设计、加锁,或者在特殊场景下使用多例。
如果继续问生命周期,可以补一句:
Spring 会先解析配置生成
BeanDefinition,然后实例化 Bean、做依赖注入、处理 Aware 接口、执行BeanPostProcessor前置、执行初始化方法、执行BeanPostProcessor后置,最后在容器关闭时销毁 Bean。AOP 代理通常发生在后置处理阶段。
6. 小结
Spring Bean 线程安全问题的核心不是singleton这个词,而是对象状态。
只要记住这条线就够了:默认单例 → 多线程共享同一个 Bean → 无状态通常安全 → 有可变成员变量就要处理并发问题 → 生命周期里后置处理可能生成代理对象。
