Spring IoC singleton 和 prototype 有什么核心区别?分别在什么场景下使用?

发布于:2025-07-09 ⋅ 阅读:(21) ⋅ 点赞:(0)

singletonprototype 是 Spring 中最基础也是最重要的两种作用域,理解它们的区别有助于我们快速掌握 Spring IoC 。

核心区别:一个共享,一个独享

可以把它们想象成餐厅里的餐具:

  • singleton (单例):就像是餐厅里那把唯一的、公用的汤勺。所有顾客(线程/请求)都来用这同一把汤勺盛汤。它从餐厅开门(容器启动)时就放在那,直到餐厅打烊(容器关闭)才收走。
  • prototype (原型):就像是餐厅提供的一次性筷子。每位顾客(线程/请求)来吃饭,都领一双全新的筷子。用完就扔掉,餐厅不负责回收,下一位顾客再领一双新的。

这个比喻引出了它们的核心区别,我们可以从以下几个维度来详细对比:


Singleton vs. Prototype 核心区别对比

特性 singleton (单例) prototype (原型/多例)
实例数量 一个容器,一个实例。全局共享。 每次请求,一个新实例。互相独立。
状态管理 必须是无状态的(Stateless)。绝对不能持有与特定调用者相关的状态数据,否则会引发线程安全问题。 通常是有状态的(Stateful)。每个实例都可以安全地保存自己的状态,因为它不会被共享。
生命周期管理 由 Spring 容器完整管理:创建、初始化、依赖注入,直到容器关闭时执行销毁(@PreDestroy)逻辑。 由 Spring 容器部分管理:容器只负责创建、初始化和注入。一旦将实例交给客户端,容器就不再跟踪它,也不会调用其销毁方法
性能和内存 性能高,内存占用低。因为实例被复用,避免了频繁创建和垃圾回收的开销。 性能和内存开销相对较高。每次都需要创建新对象,如果对象复杂,开销会很明显。
默认行为 是 Spring 的默认作用域 不是默认,需要用 @Scope("prototype") 显式声明。

分别在什么场景下使用?

场景一:使用 singleton (绝大多数情况)

singleton 是默认选项,也是最常用的选项。你应该在以下场景中使用它:

  1. 无状态的业务逻辑组件

    • Service 层UserService, ProductService 等。这些类只包含业务方法,不存储任何特定于用户的临时数据。
    • Repository/DAO 层UserRepository, ProductDao 等。它们通常是无状态的,负责与数据库交互。
    • Controller 层:Spring MVC 中的 Controller 默认就是单例的,处理 HTTP 请求。
  2. 共享的、昂贵的资源

    • 配置类:如 DataSource(数据库连接池)、RestTemplateObjectMapper 等。这些对象创建成本高,并且被设计为线程安全的,理应在整个应用中共享。
    • 工具类:各种 UtilsHelper 类。

原则: 只要一个类不为任何特定的请求或用户保存数据,它就应该是 singleton。这是构建高性能、可伸缩应用的基石。

场景二:使用 prototype (特殊情况)

当你的对象需要独立的状态时,就必须使用 prototype

  1. 需要保存状态的对象

    • 最经典的例子:购物车(ShoppingCart。每个用户都有自己独立的购物车,里面装着不同的商品。如果购物车是单例的,所有用户的商品都会混在一起,那将是一场灾难。
    @Component
    @Scope("prototype")
    public class ShoppingCart {
        private List<Item> items = new ArrayList<>();
    
        public void addItem(Item item) {
            items.add(item);
        }
        // ...
    }
    
  2. 非线程安全的对象

    • 当你需要使用一个非线程安全的第三方库对象时,为了避免并发问题,可以每次都创建一个新实例来处理请求,保证线程隔离。例如,某些老的 XML 解析器或日期格式化工具。
  3. 动态创建的任务处理器

    • 比如一个 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 实例:

  1. 依赖查找(不推荐):注入 ApplicationContext,每次需要时手动 getBean()。这会使代码与 Spring API 耦合。
  2. 使用 ObjectFactoryProvider(推荐):这是标准的 JSR-330 解决方案。
@Service
public class UserService {
    @Autowired
    private ObjectFactory<ShoppingCart> cartFactory;

    public void doShopping() {
        // 每次调用都会创建一个新的ShoppingCart实例
        ShoppingCart cart = cartFactory.getObject(); 
        cart.addItem(...);
    }
}
  1. 使用方法注入(Lookup Method Injection):这是一种比较“古老”但有效的方式,通常需要借助 CGLIB。

总结:

  • singleton 构建应用的无状态骨架
  • prototype 填充需要独立状态的血肉
  • 尽量避免在 singleton 中直接注入 prototype 带来的问题。

网站公告

今日签到

点亮在社区的每一天
去签到