singleton
和 prototype
是 Spring 中最基础也是最重要的两种作用域,理解它们的区别有助于我们快速掌握 Spring IoC 。
核心区别:一个共享,一个独享
可以把它们想象成餐厅里的餐具:
singleton
(单例):就像是餐厅里那把唯一的、公用的汤勺。所有顾客(线程/请求)都来用这同一把汤勺盛汤。它从餐厅开门(容器启动)时就放在那,直到餐厅打烊(容器关闭)才收走。prototype
(原型):就像是餐厅提供的一次性筷子。每位顾客(线程/请求)来吃饭,都领一双全新的筷子。用完就扔掉,餐厅不负责回收,下一位顾客再领一双新的。
这个比喻引出了它们的核心区别,我们可以从以下几个维度来详细对比:
Singleton vs. Prototype 核心区别对比
特性 | singleton (单例) |
prototype (原型/多例) |
---|---|---|
实例数量 | 一个容器,一个实例。全局共享。 | 每次请求,一个新实例。互相独立。 |
状态管理 | 必须是无状态的(Stateless)。绝对不能持有与特定调用者相关的状态数据,否则会引发线程安全问题。 | 通常是有状态的(Stateful)。每个实例都可以安全地保存自己的状态,因为它不会被共享。 |
生命周期管理 | 由 Spring 容器完整管理:创建、初始化、依赖注入,直到容器关闭时执行销毁(@PreDestroy )逻辑。 |
由 Spring 容器部分管理:容器只负责创建、初始化和注入。一旦将实例交给客户端,容器就不再跟踪它,也不会调用其销毁方法。 |
性能和内存 | 性能高,内存占用低。因为实例被复用,避免了频繁创建和垃圾回收的开销。 | 性能和内存开销相对较高。每次都需要创建新对象,如果对象复杂,开销会很明显。 |
默认行为 | 是 Spring 的默认作用域。 | 不是默认,需要用 @Scope("prototype") 显式声明。 |
分别在什么场景下使用?
场景一:使用 singleton
(绝大多数情况)
singleton
是默认选项,也是最常用的选项。你应该在以下场景中使用它:
无状态的业务逻辑组件:
- Service 层:
UserService
,ProductService
等。这些类只包含业务方法,不存储任何特定于用户的临时数据。 - Repository/DAO 层:
UserRepository
,ProductDao
等。它们通常是无状态的,负责与数据库交互。 - Controller 层:Spring MVC 中的 Controller 默认就是单例的,处理 HTTP 请求。
- Service 层:
共享的、昂贵的资源:
- 配置类:如
DataSource
(数据库连接池)、RestTemplate
、ObjectMapper
等。这些对象创建成本高,并且被设计为线程安全的,理应在整个应用中共享。 - 工具类:各种
Utils
或Helper
类。
- 配置类:如
原则: 只要一个类不为任何特定的请求或用户保存数据,它就应该是 singleton
。这是构建高性能、可伸缩应用的基石。
场景二:使用 prototype
(特殊情况)
当你的对象需要独立的状态时,就必须使用 prototype
。
需要保存状态的对象:
- 最经典的例子:购物车(
ShoppingCart
)。每个用户都有自己独立的购物车,里面装着不同的商品。如果购物车是单例的,所有用户的商品都会混在一起,那将是一场灾难。
@Component @Scope("prototype") public class ShoppingCart { private List<Item> items = new ArrayList<>(); public void addItem(Item item) { items.add(item); } // ... }
- 最经典的例子:购物车(
非线程安全的对象:
- 当你需要使用一个非线程安全的第三方库对象时,为了避免并发问题,可以每次都创建一个新实例来处理请求,保证线程隔离。例如,某些老的 XML 解析器或日期格式化工具。
动态创建的任务处理器:
- 比如一个
Runnable
任务,每次执行时都需要携带不同的参数。你可以将这个Runnable
定义为prototype
Bean,每次从容器中获取一个新实例,设置好参数,然后扔到线程池里执行。
- 比如一个
一个重要的“陷阱”:在 Singleton 中注入 Prototype
这是一个非常常见的面试题和易错点。
问题:如果你在一个 singleton
Bean(如 UserService
)中注入一个 prototype
Bean(如 ShoppingCart
),会发生什么?
@Service // Singleton (默认)
public class UserService {
@Autowired
private ShoppingCart cart; // Prototype
// ...
}
答案:ShoppingCart
只会创建一次!
原因:UserService
是一个单例,它在容器启动时被创建和初始化一次。在它初始化进行依赖注入时,它向容器请求了一个 ShoppingCart
。容器创建了一个新的 ShoppingCart
实例并注入给 UserService
。此后,UserService
就一直持有这个 ShoppingCart
实例的引用,再也不会去请求新的了。
这完全违背了我们使用 prototype
的初衷。
如何解决?
有几种方法可以确保每次都能从 singleton
Bean 中获取到新的 prototype
实例:
- 依赖查找(不推荐):注入
ApplicationContext
,每次需要时手动getBean()
。这会使代码与 Spring API 耦合。 - 使用
ObjectFactory
或Provider
(推荐):这是标准的 JSR-330 解决方案。
@Service
public class UserService {
@Autowired
private ObjectFactory<ShoppingCart> cartFactory;
public void doShopping() {
// 每次调用都会创建一个新的ShoppingCart实例
ShoppingCart cart = cartFactory.getObject();
cart.addItem(...);
}
}
- 使用方法注入(Lookup Method Injection):这是一种比较“古老”但有效的方式,通常需要借助 CGLIB。
总结:
- 用
singleton
构建应用的无状态骨架。 - 用
prototype
填充需要独立状态的血肉。 - 尽量避免在
singleton
中直接注入prototype
带来的问题。