Spring 中的 Bean 有哪些作用域?单例 Bean 在多线程环境下会有线程安全问题吗?为什么?
一、Spring 中 Bean 的作用域
singleton(单例,默认)
整个 Spring 容器中,该 Bean 只会被创建一次,所有请求(无论是哪个线程、哪个组件)获取到的都是同一个实例。
生命周期与容器一致:容器初始化时创建(默认懒加载可通过 @Lazy 关闭),容器销毁时销毁。
prototype(原型)
每次请求(如 getBean() 或依赖注入)时,容器都会创建一个新的 Bean 实例。
容器只负责创建,不负责销毁:实例交给调用方管理,不会随容器销毁而回收。
request(请求,仅 Web 环境)
每个 HTTP 请求对应一个独立的 Bean 实例,请求结束后实例销毁。
常用于存储请求级别的数据(如当前请求的用户信息)。
session(会话,仅 Web 环境)
每个 HTTP Session 对应一个 Bean 实例,会话过期后实例销毁。
常用于存储会话级别的数据(如用户登录状态)。
application(应用,仅 Web 环境)
整个 Web 应用(ServletContext)共享一个 Bean 实例,与应用生命周期一致。
类似单例,但作用域限定在 Web 应用内(区别于 singleton 的容器级别)。
websocket(WebSocket,仅 WebSocket 环境)
每个 WebSocket 连接对应一个 Bean 实例,连接关闭后销毁。
二、单例 Bean 在多线程环境下的线程安全问题
单例 Bean 本身不具备线程安全性,是否有线程安全问题取决于其内部状态的设计,具体分析如下:
无状态的单例 Bean 是线程安全的
若 Bean 中没有成员变量(或成员变量是常量、不可变对象),即 “无状态”,则多线程并发访问时不会有问题。
例如:一个仅包含查询逻辑的 Service,所有操作都基于方法参数,不修改任何全局状态,此时多线程访问安全。
有状态的单例 Bean 存在线程安全问题
若 Bean 中包含可修改的成员变量(即 “有状态”),多线程并发修改时会导致数据错乱,因为所有线程操作的是同一个实例的变量。
@Servicepublic class UserService {// 成员变量(状态)private String currentUser;public void setCurrentUser(String user) {this.currentUser = user; // 多线程并发修改时,会覆盖彼此的值}}
此时,线程 A 调用 setCurrentUser("A") 后,线程 B 立即调用 setCurrentUser("B"),可能导致线程 A 后续读取到的 currentUser 变为 “B”,出现数据不一致。
三、原因总结
单例 Bean 是 “全局唯一实例”,多线程共享该实例。
若 Bean 无状态(无共享变量或变量不可变),线程间没有竞争资源,因此安全。
若 Bean 有状态(存在可修改的共享变量),多线程并发修改时会产生竞态条件(Race Condition),导致线程安全问题。
解决思路:
避免在单例 Bean 中定义可修改的成员变量,尽量设计为无状态(推荐)。
若必须使用状态,可通过 ThreadLocal 存储线程私有变量(每个线程操作自己的副本)。
改用 prototype 作用域(但会增加对象创建开销,需权衡)。