Spring Bean 是單例的嗎?如何保證并發安全?
引言
面試中,經常會被問到這樣一個問題:“Spring Bean 是單例的嗎?如果是單例如何保證并發安全呢?”,這兩個問題看似沒有關聯,其實一點也不挨著,為什么呢?請聽我來“狡辯”。
首先,單例Bean 本身并不會直接導致線程安全問題。真正影響線程安全性的因素是該單例對象是否包含共享可變狀態,以及在進行并發訪問時會不會因為共享可變狀態而造成了數據不一致的現象。
其實,面試官問這樣一個問題,大約是想考察以下幾點內容:
- 對Spring Bean 作用域的理解
- 并發編程相關的知識(線程安全、同步機制等)
- 實際項目中類似問題的處理經驗
好了,了解了面試官的意圖后,我們從這幾個方面來看下這個問題應該如何回答。
Spring 中 Bean 的作用域
Spring 官方定義的Bean Scopes 有如下幾種,出自Spring 5.1.6.RELEASE 版本文檔
圖片
Bean scopes
- singleton:Spring 中默認的作用域。被定義為singleton 的Bean 實例,在第一次被請求獲取時創建出來,并緩存起來供后續使用。也就是說,在整個Spring 容器中,該Bean 只有一個實例存在。無論你多少次請求這個Bean,Spring 都會返回同一個對象實例。
- prototype:表示每次獲取該Bean 時,Spring 容器都會創建一個新的實例。這種作用域適用于那些不應該被共享的對象,例如有狀態的Bean。
- request:在Web 應用程序中,request 作用域的Bean 在一次HTTP 請求期間有效。對于每個新的HTTP 請求,Spring 會創建該Bean 的一個新實例。一旦請求完成,Bean 就會被銷毀。每個請求都有其獨立的Bean 實例,這非常適合處理與特定請求相關的狀態信息,如表單數據或用戶認證信息。
- session:類似于request 作用域,但session 作用域的Bean 在一個HTTP Session 期間有效。也就是說,在同一個用戶Session 內,所有對這個Bean 的請求都將共享同一個實例;而當Session 結束時,Bean 也會被銷毀。適用于存儲用戶的會話等相關信息。
- application:這個作用域的Bean 在ServletContext(即整個Web 應用程序)的生命周期內有效。這意味著在整個應用程序運行期間,只會存在一個這樣的Bean 實例,類似于singleton,但它是在ServletContext 中唯一的,而不是在整個Spring 容器中唯一的。
- websocket:這個作用域的Bean 在WebSocket 連接期間有效。意思是每個WebSocket 連接都有自己的Bean 實例,這些實例僅在該連接存活期間可用。當WebSocket 連接關閉時,Bean 實例將被銷毀。
Spring 中也支持自定義 Scope(這里不做贅述):
- 實現 org.springframework.beans.factory.config.Scope 接口
- 調用 org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope 方法注冊到容器中
單例 Bean 如何保證并發安全?
線程安全問題引發因素
- 多線程:多線程的運行環境,同一個程序中,多個線程并發執行。
- 產生競態條件:當一個對象內部包含可變狀態時,就可能產生競態條件(Race Condition),即不同線程之間的操作順序會影響最終結果。
示例:假設有一個單例Bean 處理訂單業務邏輯,并且它維護了一個內部計數器來跟蹤訂單數量。如果不采取任何同步措施,多個線程同時調用該placeOrder 方法可能會導致計數器值錯誤。
@Component
public class OrderService {
private int orderCount = 0;
public void placeOrder() {
orderCount++; // 可能發生競態條件
System.out.println("Placed order, total orders: " + orderCount);
}
}
解決方案
- 無狀態設計:盡量設計成無狀態的Bean,即Bean 不持有任何可變狀態。這樣即使多個線程同時訪問也不會有問題。對于確實需要維護狀態的情況,可以通過參數傳遞或外部化狀態來實現。
@Component
public class StatelessOrderService {
// 不再維護訂單狀態
public void placeOrder(Order order) {
// 訂單處理邏輯
System.out.println("Placed order: " + order.getId());
}
}
什么是無狀態與有狀態對象?
無狀態的對象 (Stateless Object):無狀態對象是指那些不保持任何內部狀態的對象。它們的行為完全由方法參數決定,這意味著每次調用相同的方法并傳入相同的參數,都將得到一致的結果,而不受之前操作的影響。
有狀態的對象 (Stateful Object):有狀態對象維護內部狀態,并且這些狀態可能會影響對象的行為。這種狀態通常是通過成員變量存儲的,在對象的生命期內可以發生變化。
- 不可變對象:如果一個對象一旦創建后就不會改變,那么它自然是線程安全的。通過使用final 關鍵字確保字段不可修改,并避免對外暴露可變狀態。
public final class ImmutableOrder {
private final String id;
private final String customerName;
public ImmutableOrder(String id, String customerName) {
this.id = id;
this.customerName = customerName;
}
public String getId() {
return id;
}
public String getCustomerName() {
return customerName;
}
}
- 同步機制:對于那些需要維護內部狀態的Bean,可以通過synchronized 關鍵字來同步方法或代碼塊,從而確保同一時刻只有一個線程能夠訪問這些方法或代碼塊。還可以使用更細粒度的鎖機制,如ReentrantLock。
@Component
public class SynchronizedOrderService {
private int orderCount = 0;
public synchronized void placeOrder() {
orderCount++;
System.out.println("Placed order, total orders: " + orderCount);
}
}
- 線程安全的數據結構:使用JUC(java.util.concurrent) 提供的線程安全集合類(如ConcurrentHashMap、CopyOnWriteArrayList)和原子變量(如AtomicInteger、AtomicLong)等,可以在不加鎖的情況下完成對數值的操作,提高性能。
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ThreadSafeOrderService {
private final Map<String, Integer> orderCounts = new ConcurrentHashMap<>();
public void placeOrder(String customerId) {
orderCounts.compute(customerId, (id, count) -> count == null ? 1 : count + 1);
System.out.println("Placed order for customer " + customerId + ", total orders: " + orderCounts.get(customerId));
}
}
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class AtomicOrderService {
private final AtomicInteger orderCount = new AtomicInteger(0);
public void placeOrder() {
int currentCount = orderCount.incrementAndGet();
System.out.println("Placed order, total orders: " + currentCount);
}
}
- ThreadLocal 變量:利用ThreadLocal 提供的每個線程私有的變量副本,可以避免多個線程之間互相干擾。如果需要在線程間傳遞上下文時可以使用這種方式。
import java.util.HashMap;
import java.util.Map;
@Component
public class ThreadLocalOrderService {
private static final ThreadLocal<Map<String, Integer>> threadLocalOrders = ThreadLocal.withInitial(HashMap::new);
public void placeOrder(String customerId) {
Map<String, Integer> orders = threadLocalOrders.get();
orders.merge(customerId, 1, Integer::sum);
System.out.println("Placed order for customer " + customerId + ", total orders in this thread: " + orders.get(customerId));
}
}
結語
當然,前面提到的一些控制并發的手段(如同步機制、原子變量、ThreadLocal 等)可以在一定程度上幫助解決本地線程安全問題,但是在分布式服務環境中,確保并發安全的挑戰更為復雜,因為不僅需要處理單個應用程序內的多線程問題,還需要應對跨多個節點的并發訪問。彼時,我們可以借助Redis、Zookeeper 等分布式中間件來控制多個服務節點的并發。