Spring 单例 Bean 是线程安全的吗?为什么不能把用户信息保存在成员变量里?
下面是一段教学用的模拟面试。
🧑💻 面试官: Spring 单例 Bean 是线程安全的吗?
🙋♂️ 我: Spring 负责管理 Bean,所以应该是安全的。
🧑💻 面试官: Service 有一个 currentUser 字段。请求 A 写入小明,请求 B 写入小李,A 后面再读,会拿到谁?
🙋♂️ 我: 有可能拿到小李。
🧑💻 面试官: 那把字段改成 volatile,能让两个用户彼此隔离吗?
单例解决的是「有几份实例」,线程安全解决的是「共享状态怎样访问」。这两件事不能互相代替。
面试速答(60 秒版)
Spring 的 singleton 通常表示每个容器、每个 Bean 定义对应一个实例,不是全 JVM 永远只能有一个。
多个请求可以共用这个实例。如果 Bean 没有不安全的共享可变状态,比较容易安全使用;但单例作用域本身不会自动给成员变量加锁或隔离请求。
当前用户、请求参数和临时计算结果,通常应该通过参数和局部变量传递,不要保存在共享 Service 字段里。局部变量如果仍指向共享可变对象,也不能只因为“变量在方法里”就认为安全。
确实需要共享的状态,要按需求使用同步或具有明确并发契约的数据结构。volatile 解决不了两个请求共用同一个字段的业务隔离问题。

图:单例共享实例,不隔离用户。
知识点详解:只有一个实例,为什么容易串请求?
两个请求共用了同一张桌子
假设 Service 上有一个 currentUser 字段,处理请求时先写用户,随后做一些工作,最后读取它生成结果。
可能出现这样的交错:
- A 写入“小明”。
- B 写入“小李”。
- A 继续读取 currentUser,拿到“小李”。
即使每次单独测试都正确,并发时也会失败。问题不是 Spring 没创建成功,而是我们把本应属于各自请求的数据放进了共享实例。
单例的具体作用域定义见 Spring Bean scopes。

图:请求交错,用户就可能串了。
为什么加 volatile 仍然不对?
volatile 可以为读写提供相应的可见性与顺序保证。但这个例子里,A 不希望“更及时地看见 B 的用户”,而是根本不该共用 B 的用户位置。
换句话说,字段可见与请求隔离不是同一个目标。就算锁住单次赋值、锁住单次读取,中间仍可能被另一个请求插进来。
最直接的修正通常是把用户信息当参数,沿调用链传下去。每次调用使用自己的值,不再依赖一个会被其他请求覆盖的共享字段。
局部变量一定安全吗?
局部变量本身属于这次调用,但它指向的对象可能来自共享缓存或实例字段。
假设每个请求都把同一个列表赋给局部变量,然后修改列表,操作的仍然是同一份对象。变量放在哪里,不会改变对象的所有权。
因此,还需要判断这份数据由谁创建、会被谁持有,以及后续是否可变。只背“局部变量在线程栈里”不够。
换成 prototype 能彻底解决吗?
prototype 表示容器在获取该 Bean 时创建新实例,但把 prototype Bean 普通地注入 singleton,并不代表每个请求都会重新注入。
如果真正需要按请求获取实例,要采用符合需求的作用域或获取方式,也要理解代理、生命周期与异步上下文的边界。增加实例不是替代状态设计的万能办法。
ThreadLocal 同样不能随手当作隔离开关。线程复用需要清理,异步切换还涉及上下文传播;共享状态问题不应该被藏到另一个难以追踪的位置。
哪些状态可以合理共享?
不可变配置可以共享;缓存、计数器等则可以共享,但要有清楚的并发访问契约。
一个没有请求级可变字段的 Service,也可能调用不安全的共享依赖。所以检查范围应该包含依赖对象,不只看当前类有没有 setter。
本文解释 Spring 的实例与状态,不把其他语言的容器 API 写成同一实现。
面试官继续追问
如何稳定地验证这个问题?
在测试里安排 A 写入后暂停,让 B 完成写入,再恢复 A 读取。主动构造交错,比连续运行两次单请求更有说明力。
所有 Service 都必须无状态吗?
不必须。无状态更容易管理,但确实需要共享状态时,应该明确它的意义和并发契约,而不是把请求临时值随手放进去。
面试速记卡
- singleton:按容器和 Bean 定义控制实例数量。
- 线程安全:取决于共享可变状态与访问方式。
- 请求数据:优先参数、局部结果,不放共享 Service 字段。
- volatile:不能把共享字段变成每个用户各一份。
- prototype:获取时创建,不等于注入后每次请求自动重建。
公司面试真题
这道题暂未收录可核验的公司真题来源。你可以先阅读本文解析,或浏览已收录的公司面试真题。
浏览公司面试真题 →