Spring 的单例 Bean 是否有并发安全问题?
- 标签:后端、Spring
🏛 业务架构师
有。单例 Bean 是容器级单例,不是线程安全保证——所有请求共享同一个实例,只要它有可变成员变量,并发写就会产生数据错乱。
业务上的典型事故形态:有人在 Service 里写了 private int count; 或缓存用的 HashMap 字段做计数/暂存,压测一上就出现脏数据;更隐蔽的是把请求参数、用户上下文暂存到成员变量,下一个请求读到上一个用户的身份——这是资金安全级别的隐患。
我的治理分三层:
- 规范层(预防):单例 Bean 无状态。需要请求级状态就用方法参数传递、
ThreadLocal(用完必须清理,线程池复用会串数据)、或声明 prototype/request 作用域; - 并发安全容器:确实需要共享可变状态(全局计数、本地限流器)时,用
AtomicLong、ConcurrentHashMap,Spring 也提供@Scope("singleton")下的并发工具; - 架构层(根治):微服务多实例部署下,进程内状态本身就是伪共享——真正的共享状态放 Redis/DB。这也是为什么 session 作用域被淘汰:弹性扩缩容要求服务无状态。
一句话回答面试:Spring 只管单例,不管并发安全;并发安全靠"无状态设计 + 并发容器 + 状态外置"三板斧,而且最后一板斧在云原生时代越来越是正解。
🏗 系统架构师
结论:单例不等于线程安全。容器只保证「一个实例」,不保证「实例无状态」。
- 是否有并发问题取决于 Bean 有没有可变成员变量:无状态 Bean(服务、仓储,只含 final 依赖)天然线程安全,这正是默认 singleton 能支撑高并发的原因;
- 典型事故:在 Service 里加非线程安全字段做「临时缓存」(SimpleDateFormat、HashMap、普通计数器),压测并发一上立刻数据错乱;
- 处理优先级:状态外置(Redis/DB)优于 ThreadLocal(注意线程池下的泄漏,用完必须 remove)优于改 prototype(治标且掩盖设计问题)。
架构视角延伸:无状态服务是水平扩展的前提——任何请求打到任何实例结果一致,扩容、缩容、滚动发布才无副作用。我把它当架构红线执行:CI 扫描 singleton Bean 的可变字段,会话状态一律外置。另外 JDK 21 虚拟线程普及后,ThreadLocal 缓存型优化进一步失效,无状态化更是唯一正解。
💻 全栈开发专家
单例 Bean 不等于线程不安全——取决于有没有可变状态。单例只是"对象只建一份",所有请求线程共享;无状态 Bean(只有 final 依赖、不改成员变量)天然安全,这是主流 Web 服务的形态。
出问题的是带可变成员变量的单例。经典事故:把 SimpleDateFormat 声明成字段——它非线程安全,并发下解析日期错乱还不抛异常,默默算错:
java
// 错误写法
private SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
// 正确:DateTimeFormatter 不可变、线程安全
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");其他高危形态:成员变量承接请求上下文(用户数据串号)、非线程安全集合做缓存、int++ 计数。修复三板斧:换不可变对象、ThreadLocal 隔离(记得清理防泄漏)、ConcurrentHashMap/AtomicLong 等并发工具。
排查:压测复现 + 数据串号类问题优先 review Controller/Service 里的非 final 字段。我的团队规约直接要求 Service 字段一律 final,能挡住九成这类隐患。