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 -> 无状态通常安全 -> 有可变成员变量就要处理并发问题 -> 生命周期里后置处理可能生成代理对象

相关推荐
MacroZheng27 分钟前
短短几天,暴涨2.8万Star!又一款编程神器开源!
java·人工智能·后端
SamDeepThinking39 分钟前
函数式编程:用BiFunction消除多类型分支的代码重复
java·后端·面试
泯泷17 小时前
第 2 篇:设计第一套字节码:Opcode、Instruction 与 Constant Pool
前端·javascript·安全
泯泷17 小时前
第 1 篇:从 1 + 2 开始:亲手写出第一台 JSVM
前端·javascript·安全
Flittly18 小时前
【AgentScope Java新手村系列】(16)从RAG到多路检索
java·spring boot·spring
小兔崽子去哪了18 小时前
Java 生成二维码解决方案
java·后端
人活一口气1 天前
从JVM调优到MCP协议:Java全栈技术体系深度总结与企业级架构实践
java·spring boot
NE_STOP1 天前
Vibe Coding -- 完整项目案例实操
java
荣码1 天前
GraphRAG:普通RAG只能回答"点"的问题,我踩了4个坑才搞懂
java·python
SimonKing1 天前
Google第三方授权登录
java·后端·程序员