文章目錄
- 1. 核心問題:Spring 框架中的 Bean 是線程安全的嗎?
- 2. 最佳實踐與解決方案
- 禁止方案:濫用`prototype`作用域
- 推薦方案(按優先級排序)
- 3. 生產環境中的典型案例
- Case 1:訂單服務統計
- Case 2:用戶會話存儲
- 4. 關鍵總結
1. 核心問題:Spring 框架中的 Bean 是線程安全的嗎?
核心點:
- Spring 中的 Bean 默認是單例(
singleton
)的。- 默認不線程安全: Spring 框架本身不對單例 Bean 進行線程安全封裝。
- 線程安全取決于使用方式: 如果 Bean 的狀態是不可變的,或者不包含可變狀態,則在某種程度上是線程安全的。如果 Bean 包含可變狀態且多線程同時訪問,就需要考慮線程安全問題。
- 解決方案: 改變作用域(
prototype
,可多例)或自行處理線程同步。
讓我們用一個通俗的類比來理解:
類比:一個公共圖書館的書籍
- Spring 容器: 想象一下 Spring 容器是一個大型的公共圖書館。
- Bean: 圖書館里的每一本書都是一個 Bean。
- 單例 Bean: 想象一下圖書館里只有 一本 特別重要的參考書(比如《Spring 官方文檔》),所有想查閱這本書的人(線程)都必須共用這一本。這就是單例 Bean。
- 多例 Bean (Prototype): 想象一下圖書館里有很多本同一本書(比如《Java 編程入門》),每個人都可以拿一本自己的去讀。這就是多例 Bean。
- Bean 的狀態: 書籍的內容就是 Bean 的狀態。
- 可變狀態: 如果這本書允許你在上面做筆記、劃線、修改內容,那么這本書就是具有可變狀態的。
- 不可變狀態: 如果這本書不允許任何修改,只能閱讀,那么這本書就是具有不可變狀態的。
- 線程: 來圖書館查閱書籍的每個人就是一個線程。
現在,我們來套用類比來理解線程安全問題:
- 單例 Bean (只有一本參考書): 如果多個讀者(線程)同時想要在同一本參考書(單例 Bean)上做筆記(修改狀態),就會產生沖突。第一個讀者寫了一半,第二個讀者也開始寫,內容就會混亂。這就是 線程不安全。
- 多例 Bean (很多本入門書): 如果每個讀者(線程)都拿一本自己的入門書(多例 Bean),他們可以在自己的書上隨意做筆記(修改狀態),互不影響。這就是 線程安全。
- 不可變狀態的單例 Bean (只能閱讀的參考書): 如果這本參考書不允許做任何修改,多個讀者(線程)同時閱讀(訪問不可變狀態),他們不會互相干擾。盡管是同一本書,但因為內容不可修改,所以是 線程安全 的。
- 可變狀態的單例 Bean (允許做筆記的參考書): 如果這本參考書允許做筆記,多個讀者(線程)同時做筆記,就會產生線程不安全問題。
2. 最佳實踐與解決方案
禁止方案:濫用prototype
作用域
- 問題:每次請求創建新Bean實例,導致內存飆升、GC壓力增大,違背單例設計初衷。
- 適用場景:僅當Bean需要持有請求級狀態(如用戶會話數據)時使用。
推薦方案(按優先級排序)
方案 | 適用場景 | 實現方式 | 案例 |
---|---|---|---|
無狀態設計 | 絕大多數業務邏輯 | 移除成員變量,用局部變量/參數傳遞數據 | Service層業務方法 |
ThreadLocal | 線程綁定的數據(如用戶身份) | ThreadLocal<UserContext> | 權限校驗、數據庫路由 |
同步鎖(synchronized) | 低并發場景的簡單狀態 | synchronized 方法/代碼塊 | 本地計數器 |
并發工具類 | 復雜狀態管理 | AtomicInteger , ConcurrentHashMap | 分布式ID生成、緩存 |
不可變對象 | 配置類等只需初始化的數據 | 用final 修飾字段,無setter方法 | 系統參數配置Bean |
3. 生產環境中的典型案例
Case 1:訂單服務統計
@Service
public class OrderService {// 錯誤!多線程下totalOrders可能少加private long totalOrders = 0; // 正確方案:使用AtomicLongprivate final AtomicLong totalOrders = new AtomicLong(0);public void placeOrder() {// 業務邏輯...totalOrders.incrementAndGet();}
}
Case 2:用戶會話存儲
@Service
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession { // 使用request作用域替代單例private String userId;// getter/setter...
}// 更優方案:ThreadLocal(避免創建過多對象)
public class UserContext {private static final ThreadLocal<String> USER_HOLDER = new ThreadLocal<>();public static void setUserId(String id) { USER_HOLDER.set(id); }public static String getUserId() { return USER_HOLDER.get(); }
}
4. 關鍵總結
- 默認規則:Spring單例Bean非線程安全,安全與否取決于開發者的設計。
- 黃金準則:優先設計無狀態Bean,必須維護狀態時用并發工具或ThreadLocal。
- 避坑指南:
- 避免在單例Bean中定義
非final
成員變量 - 慎用
prototype
作用域 - 同步鎖范圍要最小化(鎖方法不如鎖代碼塊)
- 避免在單例Bean中定義
延伸思考:為什么Spring MVC的
Controller
默認單例卻安全?
答:Controller中處理的HttpServletRequest
和響應對象本質是每個請求獨享(由Tomcat
線程池分配),與單例Controller實例無關。