"如果 prototype Bean 内部没有共享状态,自身是线程安全的。但 Spring 容器对 prototype 的多例行为有'陷阱',可能导致实际使用中不安全。"
展开讲:
prototype 注入到 singleton 里的线程安全分析
一、prototype Bean 自身的线程安全性
1.1 prototype 本身是"独立实例"(这个是安全的)
@Service@Scope("prototype")public class ShoppingCart { private List<Item> items = new ArrayList<>(); public void addItem(Item item) { items.add(item); // 操作的是自己的成员变量 }}
为什么自身是线程安全的?
| 特性 | 解释 |
|---|---|
| 每个 prototype 实例独立 | 1000 个调用 → 1000 个 ShoppingCart 实例,互相不共享 |
| 每个实例的成员变量独立 | 线程 A 操作 cart1.items,线程 B 操作 cart2.items,互不干扰 |
| GC 独立 | 一个实例的 GC 不影响其他实例 |
**所以 prototype Bean 本身是线程安全的,前提是你每次都拿到了不同的实例。
1.2 但这里有"陷阱"------老哥 7+ 年必须知道
如果不加 proxyMode,singleton 里的 prototype 不是"真多例":
@Service // singletonpublic class OrderService { @Autowired private ShoppingCart cart; // ⚠️ 看着像 prototype}
实际行为:
OrderService是单例,整个应用只有 1 个cart在OrderService创建时注入一次- cart 永远是同一个 ShoppingCart 实例!
- 1000 个请求都共享同一个 cart → 多线程同时改 cart.items → 线程不安全!
二、3 种场景的线程安全分析
场景 1:纯 prototype(不注入到 singleton,直接 getBean)
ApplicationContext ctx = ...;ShoppingCart cart1 = ctx.getBean(ShoppingCart.class); // 实例 1ShoppingCart cart2 = ctx.getBean(ShoppingCart.class); // 实例 2(不同对象)
线程安全:✅ 是
- 每次
getBean都返回新实例 - 多个线程拿到不同实例
- 互不干扰
场景 2:prototype 注入到 singleton(不加 proxyMode)
@Service // singletonpublic class OrderService { @Autowired private ShoppingCart cart; // 注入时拿到一次}
线程安全:❌ 否
- 整个应用 1 个 OrderService
- 整个应用 1 个 cart(注入时定下来)
- 多线程共享同一 cart → 不安全
场景 3:prototype + proxyMode / @Lazy(真正多例)
@Service@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)public class ShoppingCart { ... }@Servicepublic class OrderService { @Autowired private ShoppingCart cart; // 代理对象}
线程安全:✅ 是
- 注入的是代理对象
- 每次调用 cart 方法时,代理内部创建一个新的 ShoppingCart
- 1000 个请求 → 1000 个不同 ShoppingCart → 互不干扰
三、Spring 多例的 4 个"坑"(老哥面试加分)
坑 1:注入时拿到一次(最常踩)
@Servicepublic class A { @Autowired private PrototypeBean b; // ⚠️ 整个 A 共享同一个 b}
解决: proxyMode = TARGET_CLASS 或 @Lazy
坑 2:@Async 注解的方法内 prototype 失效
@Service@Scope("prototype")public class TaskRunner { ... }@Servicepublic class TaskService { @Autowired private TaskRunner runner; @Async // 异步调用 public void run() { // 这里 runner 是多少个?看注入方式 }}
坑:
- 如果
@Autowired直接注入,runner 永远同一个 - 必须用
ObjectProvider<TaskRunner>每次.getObject()才拿到新实例
坑 3:prototype 在 @Configuration 里 @Bean 不会自动多例
@Configurationpublic class AppConfig { @Bean @Scope("prototype") public ShoppingCart cart() { return new ShoppingCart(); }}
坑:
- 这种写法是真的多例(每次 getBean 都新建)
- 但调用
cart()方法本身只返回同一个对象(因为 Spring 拦截了 @Bean 方法) - 实际多例要靠其他 Bean 注入或 getBean 触发
坑 4:prototype Bean 的销毁不归 Spring 管
@PreDestroy // ⚠️ prototype Bean 上加这个不会生效public void cleanup() { // 永远不会调用}
解决:
- 实现
DisposableBean接口 - 用
BeanPostProcessor手动管理
四、面试官追问应对
追问:prototype 注入到 singleton 里,prototype 还线程安全吗?
"分情况看:
如果用 proxyMode / @Lazy 真正多例:✅ 线程安全(每个线程拿到不同实例)。
如果直接 @Autowired 不加处理:❌ 不安全(整个应用共享同一个 prototype 实例)。
核心坑:singleton Bean 创建时 prototype 注入一次,多线程共享。
追问 2:怎么判断当前 Bean 是不是 prototype?
// 运行时判断if (ctx.containsBean("xxx") && ctx.isPrototype("xxx")) { // 是 prototype}
追问 3:怎么让 singleton 注入 prototype 真的多例?
"3 种方法:
1.
@Scope + proxyMode = TARGET_CLASS(最推荐)2.
@Lazy(延迟加载,代理对象)3.
ObjectProvider<PrototypeBean>+.getObject()(最灵活)"
追问 4:ObjectProvider 怎么用?
@Servicepublic class OrderService { @Autowired private ObjectProvider<ShoppingCart> cartProvider; // 不直接注入 public void checkout() { ShoppingCart cart = cartProvider.getObject(); // 每次调用拿到新实例 cart.addItem(...); }}
ObjectProvider 优点:
- 显式控制获取时机
- 不用加
@Scope/@Lazy/proxyMode - 项目里推荐用这个(最清晰)
六、面试答法模板
"3 句话讲清楚 prototype 线程安全:
1.prototype 本身是独立的 (每个实例不共享成员变量),所以自身线程安全。
2.但注入到 singleton Bean 里时,不加 proxyMode / @Lazy 会变成'假多例' (整个应用共享同一实例),多线程并发改这个共享实例就线程不安全。
3.**正确做法:proxyMode = TARGET_CLASS / @Lazy / ObjectProvider,**保证每次调用拿到新实例。
我做的 MOVA 报表生成器就是这个套路,proxyMode 让 100 个并发任务互不干扰。"
七、一句话总结
"prototype 自身线程安全,但注入到 singleton 不加 proxyMode 会变成'假多例',整个应用共享一个 prototype 实例,反而不安全。"