Spring默认Bean单例是为了性能与资源控制:高并发下避免重复创建、注入和代理开销,减少GC压力;其线程安全依赖无状态设计,状态应置于方法参数、RequestScope或ThreadLocal中。

Spring 默认把 @Bean 和 @Component 声明的 Bean 设为单例,不是为了“省事”,而是直奔性能和资源控制去的——尤其在 Web 容器高并发场景下,对象创建开销是实打实压垮吞吐量的瓶颈。
单例如何减少对象创建开销
每次 HTTP 请求触发 Spring 容器获取 Bean 时,单例模式直接从缓存(singletonObjects Map)返回已有实例;而原型(prototype)每次都要走完整初始化流程:反射实例化、依赖注入、后置处理器(如 Aware 回调、InitializingBean)、AOP 代理生成等。
- 反射创建对象本身就有 JVM 方法区查找 + 内存分配(TLAB 分配失败会触发同步)成本
- 若 Bean 含有
@PostConstruct或实现InitializingBean,每次都会重复执行初始化逻辑 - AOP 代理(比如
@Transactional)在原型模式下每次都要重新生成 CGLIB 子类或 JDK 动态代理,GC 压力明显上升
高并发下单例的资源收益有多明显
以一个典型 Spring Boot Web 应用为例:每秒 1000 次请求,Service 层 Bean 若设为 prototype,意味着每秒新建 1000 个对象实例 + 执行 1000 次依赖注入 + 可能 1000 次代理增强。这些对象生命周期极短,很快进入 Young GC 区域,频繁触发 Minor GC,CPU 时间花在垃圾回收上而非业务处理。
- 单例 Bean 在容器启动时一次性创建,后续请求零对象分配开销
- JVM 堆中只保留一份实例,避免堆内存碎片化和 GC 频次上升
- 数据库连接池、线程池、缓存客户端等共享资源类天然适合单例,重复创建会导致连接泄漏或资源争抢
为什么单例在高并发下不等于线程不安全
关键不在“单例”,而在“状态”。Spring 默认推荐无状态(stateless)设计:Service、DAO、Controller(不持有请求级字段)这类 Bean 本身不保存可变成员变量,多个线程共用一个实例不会产生数据污染。
- 错误示范:
private int counter = 0;这种字段在单例里就是并发陷阱 - 正确做法:把请求相关状态放在方法参数、
@RequestScopeBean 或ThreadLocal中 - Spring 自身的事务管理器、JdbcTemplate 等都是单例且线程安全的——它们靠的是底层 JDBC Connection 的按需获取,而非在 Bean 实例里存 Connection
什么时候必须放弃单例
不是所有 Bean 都适合单例。一旦出现以下情况,就得切到 prototype 或更细粒度的作用域:
- Bean 内部持有可变的、与请求/会话强绑定的状态(如临时文件句柄、用户会话上下文缓存)
- 需要隔离测试环境,比如单元测试中反复重置 Bean 状态
- 集成非线程安全的第三方库,且无法包装或改造其线程模型
- Web 场景下,明确要每个请求独享一个 Bean 实例(用
@Scope("request")比prototype更合适,它由 Web 容器管理生命周期)
真正容易被忽略的点是:单例的“安全”完全依赖开发者对状态边界的控制。哪怕加了 @Scope("prototype"),如果内部又通过静态变量或外部缓存偷偷共享状态,照样出问题。

















