知識庫 / Java Concurrency RSS 訂閱

Spring單例Bean如何處理併發請求?

Java Concurrency,Spring
HongKong
9
12:16 PM · Dec 06 ,2025

1. 概述

在本教程中,我們將學習如何 Spring beans 創建時,帶有 singleton 作用域的工作原理,以服務多個併發請求。 此外,我們還將瞭解 Java 如何在內存中存儲 bean 實例以及如何處理對其進行併發訪問的方式。

2. Spring Bean 和 Java 堆內存

Java 堆,正如我們所知,是應用程序中所有正在運行的線程可以訪問的全局共享內存。當 Spring 容器創建一個具有 singleton 作用域的 Bean 時,該 Bean 會存儲在堆中。 這樣,所有併發線程都可以指向同一個 Bean 實例。

接下來,讓我們瞭解線程的堆棧內存及其如何幫助處理併發請求。

3. 如何處理併發請求?

例如,我們以一個 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

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 確保並行執行的線程不會互相覆蓋彼此的變量。

其次,由於 ProductService Bean 沒有在堆級別設置任何限制或鎖,每個線程的程序計數器可以指向堆內存中 Bean 實例的相同引用。 因此,兩個線程可以同時執行 getProdcutById() 方法。

接下來,我們將理解為什麼單例 Bean 應該是無狀態的。

4. 無狀態單例 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 時,productName 顯示的是“Product 2”,而不是“Product 1”。這是因為 ProductService 是狀態化的,並且與所有正在運行的線程共享相同的 productName 變量。

為了避免出現類似的不良副作用,保持我們的單例 Bean 無狀態至關重要。

5. 結論

在本文中,我們瞭解了在 Spring 中併發訪問單例 Bean 的工作原理。首先,我們考察了 Java 如何在堆內存中存儲單例 Bean。然後,我們學習了不同線程如何從堆內存中訪問相同的單例實例。最後,我們討論了無狀態 Bean 的重要性,並提供了一個如果 Bean 不是無狀態的可能發生的情況的示例。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.