Spring 单例 Bean 如何处理并发请求?

1、Spring Bean 和 Java 堆内存

Java 堆是一个全局共享内存,应用中的所有运行线程都可以访问它。当 Spring 容器创建 Singleton Scope Bean 时,该 Bean 将存储在堆中。这样,所有并发线程都能指向同一个 Bean 实例。

2、如何处理并发请求?

举例来说,Spring 应用中有一个名为 ProductService 的单例 Bean:

@Service
public class ProductService {
    private final static List<Product> productRepository = asList(
      new Product(1, "Product 1", new Stock(100)),
      new Product(2, "Product 2", new Stock(50))
    );

    public Optional<Product> getProductById(int id) {
        Optional<Product> product = productRepository.stream()
          .filter(p -> p.getId() == id)
          .findFirst();
        String productName = product.map(Product::getName)
          .orElse(null);

        System.out.printf("Thread: %s; bean instance: %s; product id: %s has the name: %s%n", currentThread().getName(), this, id, productName);

        return product;
    }
}

该 Bean 有一个 getProductById() 方法,用于向调用者返回产品数据。此外,该 Bean 返回的数据将通过 /product/{id} 端点向客户公开。

接下来,让我们来看看运行时同时调用 /product/{id} 端点会发生什么情况。具体来说,第一个线程将调用 /product/1 端点,第二个线程将调用 /product/2

服务器会为每个请求创建不同的线程。正如我们在下面的控制台输出中所看到的,两个线程都使用相同的 ProductService 实例来返回产品数据:

Thread: pool-2-thread-1; bean instance: com.baeldung.concurrentrequest.ProductService@18333b93; product id: 1 has the name: Product 1
Thread: pool-2-thread-2; bean instance: com.baeldung.concurrentrequest.ProductService@18333b93; product id: 2 has the name: Product 2

Spring 可以在多个线程中使用同一个 Bean 实例,这首先是因为 Java 会为每个线程创建一个私有栈内存。

栈内存负责存储线程执行过程中方法内部使用的局部变量的状态。这样,Java 就能确保并行执行的线程不会覆盖彼此的变量。

其次,由于 ProductService Bean 在堆级别未设置任何限制或锁,因此每个线程的程序计数器(program counter)都能指向堆内存中 Bean 实例的相同引用。因此,两个线程可以同时执行 getProdcutById() 方法。

接下来,来了解一下为什么 singleton bean 必须是无状态的?

3、无状态单例 Bean 与有状态单例 Bean

要了解无状态单例 Bean 的重要性,先来看看使用有状态单例 Bean 会有什么问题。

假设,将 productName 变量移到了类级别:

@Service
public class ProductService {
    private String productName = null;
    
    // ...

    public Optional getProductById(int id) {
        // ...

        productName = product.map(Product::getName).orElse(null);

       // ...
    }
}

现在,再次运行服务并查看输出结果:

Thread: pool-2-thread-2; bean instance: com.baeldung.concurrentrequest.ProductService@7352a12e; product id: 2 has the name: Product 2
Thread: pool-2-thread-1; bean instance: com.baeldung.concurrentrequest.ProductService@7352a12e; product id: 1 has the name: Product 2

可以看到,对 productId 1 的调用显示的 productNameProduct 2,而不是 Product 1。出现这种情况是因为 ProductService 是有状态的,它与所有运行线程共享同一个 productName 变量。

为了避免这种问题,让单例 Bean 保持无状态至关重要。

4、总结

本文介绍了 Spring 中单例 Bean 是如何处理并发请求的,以及使用无状态单例 Bean 的重要性。


参考:https://www.baeldung.com/spring-singleton-concurrent-requests