1. 概述

在这个教程中,我们将学习如何在幕后使用具有singleton作用域的Spring Bean处理多个并发请求。此外,我们将理解Java如何在内存中存储Bean实例,并处理对它们的并发访问。

2. Spring Bean与Java堆内存

我们都知道,Java堆是应用程序中所有运行线程共享的全局内存。当Spring容器创建一个具有singleton作用域的Bean时,该Bean会存储在堆中。这样,所有并发线程都可以指向同一个Bean实例。

接下来,让我们了解线程栈内存的作用以及它如何帮助处理并发请求。

3. 如何处理并发请求?

以一个名为ProductService的singleton 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

Spring为每个请求创建不同的线程。如下面的控制台输出所示,两个线程都使用同一个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为何至关重要。

4. 无状态singleton Bean与有状态singleton Bean

为了理解无状态singleton Bean的重要性,让我们看看使用有状态singleton 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的调用显示了productName为"Product 2"而不是"Product 1"。这是因为ProductService是有状态的,与所有运行线程共享productName变量。

为了避免此类意外的副作用,保持singleton Bean无状态至关重要。

5. 总结

在这篇文章中,我们了解了Spring中如何处理对singleton Bean的并发访问。首先,我们研究了Java如何在堆内存中存储singleton Bean。接着,我们学习了不同线程如何从堆中访问相同的singleton实例。最后,我们讨论了为什么无状态Bean很重要,并且举例说明了如果Bean不是无状态的,可能会发生什么。

如往常一样,这些示例的代码可以在GitHub上找到。