對Spring中bean線程安全的討論
Spring容器中的Bean是否線程安全,容器本身并沒有提供Bean的線程安全策略,因此Spring容器中的Bean本身不具備線程安全的特性,但是具體要結(jié)合具體的scope、靜態(tài)變量、常量、成員變量等多種屬性去研究。
1、Bean狀態(tài)介紹
1.1、有狀態(tài)對象
有實(shí)例變量的對象,即每個用戶最初都會得到一個初始的bean,可以保存數(shù)據(jù),是非線程安全的。
每個用戶有自己特有的一個實(shí)例,在用戶的生存期內(nèi),bean保持了用戶的信息,即“有狀態(tài)”;一旦用戶滅亡(調(diào)用結(jié)束或?qū)嵗Y(jié)束),bean的生命期也告結(jié)束。
代碼示例:
@Service public class Counter { private int count = 0; // 有狀態(tài):保存實(shí)例變量 public void increment() { count++; // 非原子操作,線程不安全 } public int getCount() { return count; } }
- 多個線程調(diào)用 increment() 時,count++操作可能因指令重排或并發(fā)寫入導(dǎo)致數(shù)據(jù)不一致。
1.2、無狀態(tài)對象
沒有實(shí)例變量的對象,不能保存數(shù)據(jù),是不變類,是線程安全的。
- bean一旦實(shí)例化就被加進(jìn)會話池中,各個用戶都可以共用。即使用戶已經(jīng)消亡,bean 的生命期也不一定結(jié)束,它可能依然存在于會話池中,供其他用戶調(diào)用。
- 由于沒有特定的用戶,那么也就不能保持某一用戶的狀態(tài),所以叫無狀態(tài)bean。但無狀態(tài)會話bean 并非沒有狀態(tài),
- 如果有自己的屬性(變量),那么這些變量就會受到所有調(diào)用它的用戶的影響。
代碼示例如下:
@Service public class Calculator { // 無狀態(tài):不保存任何實(shí)例變量 public int add(int a, int b) { return a + b; } }
- 多個線程調(diào)用add(1,2)時,結(jié)果不會互相影響。
兩者的區(qū)別和聯(lián)系:
2、Bean作用域
bean的生命周期如下所示:
實(shí)例化--->設(shè)置屬性--->初始化--->銷毀
Spring 的 bean 作用域(scope)類型:
1、singleton:單例,默認(rèn)作用域。
- 優(yōu)點(diǎn): 節(jié)省內(nèi)存,因?yàn)橹淮嬖谝粋€實(shí)例。
- 缺點(diǎn): 由于多個線程可能共享同一個實(shí)例,需要格外注意線程安全(非線程安全的狀態(tài)字段可能導(dǎo)致問題)
2、prototype:原型,每次創(chuàng)建一個新對象。
3、request:請求,每次Http請求創(chuàng)建一個新對象,適用于WebApplicationContext環(huán)境下。
4、session:會話,同一個會話共享一個實(shí)例,不同會話使用不用的實(shí)例。
5、global-session:全局會話,所有會話共享一個實(shí)例。
3、線程安全:
從單例與原型Bean分別進(jìn)行說明。
3.1、bean的分類
1、原型Bean
對于原型Bean,每次創(chuàng)建一個新對象,也就是線程之間并不存在Bean共享,自然是不會有線程安全的問題。
2、單例Bean
對于單例Bean,所有線程都共享一個單例實(shí)例Bean,因此是存在資源的競爭。
3.2、bean的安全
1、@Controller相關(guān)
可以這樣理解:
如果單例Bean,是一個無狀態(tài)Bean,在線程中的操作不會對Bean的成員執(zhí)行查詢以外的操作,那么這個單例Bean是線程安全的。
比如Spring mvc 的 Controller、Service、Dao等,這些Bean大多是無狀態(tài)的,默認(rèn)情況下@Controller沒有加上@Scope,默認(rèn)Scope就是默認(rèn)值singleton,單例的 ,系統(tǒng)只會初始化一次 Controller 容器,只關(guān)注于方法本身。
但是,如果每次請求的都是同一個 Controller 容器里面的非線程安全的字段,那么就不是線程安全的。
代碼示例:
@RestController public class TestController { //非線程安全的字段 private int var = 0; @GetMapping(value = "/test_var") public String test() { System.out.println("普通變量var:" + (++var)); return "普通變量var:" + var ; } } 輸出: 普通變量var:1 普通變量var:2 普通變量var:3
修改了作用于改為:prototype
每個請求都單獨(dú)創(chuàng)建一個 Controller 容器,所以各個請求之間是線程安全的。
@RestController @Scope(value = "prototype") // 加上@Scope注解,有2個取值:單例-singleton 多實(shí)例-prototype public class TestController { private int var = 0; @GetMapping(value = "/test_var") public String test() { System.out.println("普通變量var:" + (++var)); return "普通變量var:" + var ; } } 輸出: 普通變量var:1 普通變量var:1 普通變量var:1
總結(jié)
1、@Controller/@Service 等容器中,默認(rèn)情況下,scope值是單例- singleton 的,是線程不安全的。
2、盡量不要在 @Controller/@Service 等容器中定義靜態(tài)變量,不論是單例( singleton )還是多實(shí)例( prototype )都是線程不安全的。
3、默認(rèn)注入的Bean對象,在不設(shè)置scope的時候也是線程不安全的。
4、一定要定義變量的話,用 ThreadLocal 來封裝,這個是線程安全的。
2、@prototype注解
@Scope 注解的 prototype 實(shí)例一定就是線程安全的嗎?
答案是否定的。上面已經(jīng)解釋過了,需要根據(jù)多方位去考量。
@RestController @Scope(value = "prototype") // 加上@Scope注解,有2個取值:單例-singleton 多實(shí)例-prototype public class TestController { private int var = 0; //只會初始化一次,因此也非線程安全的變量 private static int staticVar = 0; ? @GetMapping(value = "/test_var") public String test() { System.out.println("普通變量var:" + (++var)+ "---靜態(tài)變量staticVar:" + (++staticVar)); return "普通變量var:" + var + "靜態(tài)變量staticVar:" + staticVar; } } 輸出: 普通變量var:1---靜態(tài)變量staticVar:1 普通變量var:1---靜態(tài)變量staticVar:2 普通變量var:1---靜態(tài)變量staticVar:3
總結(jié):線程安全在于怎樣去定義變量以及 Controller 的配置。
示例:
config里面自己定義的Bean: User
@Configuration public class MyConfig { @Bean public User user(){ return new User(); } }
@RestController @Scope(value = "singleton") // prototype singleton public class TestController { ? private int var = 0; // 定義一個普通變量 ? private static int staticVar = 0; // 定義一個靜態(tài)變量 ? @Value("${test-int}") private int testInt; // 從配置文件中讀取變量 ? ThreadLocal<Integer> tl = new ThreadLocal<>(); // 用ThreadLocal來封裝變量 ? @Autowired private User user; // 注入一個對象來封裝變量 ? @GetMapping(value = "/test_var") public String test() { tl.set(1); System.out.println("先取一下user對象中的值:"+user.getAge()+"===再取一下hashCode:"+user.hashCode()); user.setAge(1); System.out.println("普通變量var:" + (++var) + "===靜態(tài)變量staticVar:" + (++staticVar) + "===配置變量testInt:" + (++testInt) + "===ThreadLocal變量tl:" + tl.get()+"===注入變量user:" + user.getAge()); return "普通變量var:" + var + ",靜態(tài)變量staticVar:" + staticVar + ",配置讀取變量testInt:" + testInt + ",ThreadLocal變量tl:" + tl.get() + "注入變量user:" + user.getAge(); } }
輸出:
先取一下user對象中的值:0===再取一下hashCode:241165852
普通變量var:1===靜態(tài)變量staticVar:1===配置變量testInt:1===ThreadLocal變量tl:1===注入變量user:1
先取一下user對象中的值:1===再取一下hashCode:241165852
普通變量var:2===靜態(tài)變量staticVar:2===配置變量testInt:2===ThreadLocal變量tl:1===注入變量user:1
先取一下user對象中的值:1===再取一下hashCode:241165852
普通變量var:3===靜態(tài)變量staticVar:3===配置變量testInt:3===ThreadLocal變量tl:1===注入變量user:1
在單例模式下 Controller 中只有用 ThreadLocal 封裝的變量是線程安全的??梢钥吹?次請求結(jié)果里面只有 ThreadLocal 變量值每次都是從 0+1=1 的,其他的幾個都是累加的,而 user 對象呢,默認(rèn)值是0,第二交取值的時候就已經(jīng)是1了,關(guān)鍵它的 hashCode 是一樣的,說明每次請求調(diào)用的都是同一個 user 對象。
TestController 上的 @Scope 注解的屬性改一下改成多實(shí)例的: @Scope(value = "prototype") ,其他都不變,再次請求,結(jié)果如下:
public class MyConfig { @Bean @Scope(value = "prototype") public User user(){ return new User(); } }
@RestController @Scope(value = "prototype") // prototype singleton public class TestController { ? private int var = 0; // 定義一個普通變量 ? private static int staticVar = 0; // 定義一個靜態(tài)變量 ? @Value("${test-int}") private int testInt; // 從配置文件中讀取變量 ? ThreadLocal<Integer> tl = new ThreadLocal<>(); // 用ThreadLocal來封裝變量 ? @Autowired private User user; // 注入一個對象來封裝變量 ? @GetMapping(value = "/test_var") public String test() { tl.set(1); System.out.println("先取一下user對象中的值:"+user.getAge()+"===再取一下hashCode:"+user.hashCode()); user.setAge(1); System.out.println("普通變量var:" + (++var) + "===靜態(tài)變量staticVar:" + (++staticVar) + "===配置變量testInt:" + (++testInt) + "===ThreadLocal變量tl:" + tl.get()+"===注入變量user:" + user.getAge()); return "普通變量var:" + var + ",靜態(tài)變量staticVar:" + staticVar + ",配置讀取變量testInt:" + testInt + ",ThreadLocal變量tl:" + tl.get() + "注入變量user:" + user.getAge(); } }
先取一下user對象中的值:0===再取一下hashCode:1612967699
普通變量var:1===靜態(tài)變量staticVar:1===配置變量testInt:1===ThreadLocal變量tl:1===注入變量user:1先取一下user對象中的值:0===再取一下hashCode:985418837
普通變量var:1===靜態(tài)變量staticVar:2===配置變量testInt:1===ThreadLocal變量tl:1===注入變量user:1先取一下user對象中的值:0===再取一下hashCode:1958952789
普通變量var:1===靜態(tài)變量staticVar:3===配置變量testInt:1===ThreadLocal變量tl:1===注入變量user:1
3、靜態(tài)變量
靜態(tài)變量的生命周期由 JVM 管理,與 Spring 無關(guān)。所有實(shí)例(單例或原型)共享同一個靜態(tài)變量。
@Component public class MyService { private static int count = 0; // 靜態(tài)變量 public void increment() { count++; // 多線程環(huán)境下可能出問題 } }
在 Spring 中,無論是單例(Singleton)作用域還是原型(Prototype)作用域的 Bean,只要在類中定義了靜態(tài)變量(static 變量),都可能存在線程安全問題。
總結(jié):多實(shí)例模式下普通變量,取配置的變量還有 ThreadLocal 變量都是線程安全的,而靜態(tài)變量和 user 對象中的變量都是非線程安全的。
4、ThreadLocal
4.1、概念
ThreadLocal 類提供了線程局部變量,每個線程可以將一個值存在 ThreadLocal 對象中,其他線程無法訪問這些值。每個線程都有自己獨(dú)立的變量副本。
ThreadLocal 的初始值可通過 withInitial() 方法設(shè)置:
private static final ThreadLocal<String> requestId = ThreadLocal.withInitial(() -> "default-id");
簡單的內(nèi)存模型:
+-----------------+ +------------------+ | Thread A | | Thread B | +-----------------+ +------------------+ | ThreadLocal | | ThreadLocal | | - value: 123 | | - value: 456 | +-----------------+ +------------------+ Thread A and Thread B can have different values in the same ThreadLocal.
不同線程直接保存了不同的值。
4.2、優(yōu)點(diǎn)
若單例 Bean 需要保存線程私有的狀態(tài)(如用戶請求上下文),多線程場景下,多個線程對這個單例Bean的成員變量并不存在資源的競爭,因?yàn)門hreadLocal為每個線程保存線程私有的數(shù)據(jù)。這是一種以空間換時間的方式。
4.3、原理
如下圖所示:
調(diào)用 ThreadLocal.set(value)方法時,它會將這個值與當(dāng)前線程關(guān)聯(lián),而該值被存儲在當(dāng)前線程的一個內(nèi)部數(shù)據(jù)結(jié)構(gòu)中。通過 ThreadLocal.get()方法,可以獲取當(dāng)前線程所關(guān)聯(lián)的值。
- 核心機(jī)制:每個線程內(nèi)部維護(hù)一個 ThreadLocalMap(類似鍵值對存儲,以 ThreadLocal 對象為鍵,存儲線程私有的變量。
- 數(shù)據(jù)隔離:線程通過自己的 ThreadLocalMap 訪問變量,不同線程之間的數(shù)據(jù)互不影響。
- 內(nèi)存模型:
Thread-1 → ThreadLocalMap → { ThreadLocalA → Value1, ThreadLocalB → Value2 } Thread-2 → ThreadLocalMap → { ThreadLocalA → Value3, ThreadLocalB → Value4 }
4.4、注意
由于ThreadLocal里面維護(hù)了ThreadLocalMap類,如下圖所示:
而TheadLocalMap是由Entry[]組成組成,Entry[]維護(hù)了多個entry。如下所示:
一個entry由key(threadlocal)和value,Entry繼承了弱引用,關(guān)于弱引用可參考:對Java 資源管理和引用體系的介紹
如下圖所示:entry
如果使用不當(dāng),會引發(fā)oom問題,主要是由GC回收機(jī)制和內(nèi)存結(jié)構(gòu)兩者引起。
4.5、使用場景
- 用戶會話信息: 在 web 應(yīng)用中維護(hù)用戶的會話信息,避免將狀態(tài)信息寫到全局上下文。
- 數(shù)據(jù)庫連接: 在線程中維護(hù)數(shù)據(jù)源連接,避免不同線程之間共享資源引起的競爭。
- 事務(wù)管理(如 Spring 的 TransactionSynchronizationManager)。
以下是一個簡單的 Spring Bean 示例,展示如何在 Spring 中使用 ThreadLocal 來存儲用戶會話信息。
1.定義一個 ThreadLocal Storage
import org.springframework.stereotype.Component; @Component public class UserContext { private static final ThreadLocal<String> userHolder = new ThreadLocal<>(); public void setCurrentUser(String username) { userHolder.set(username); } public String getCurrentUser() { return userHolder.get(); } //清理 ThreadLocal,防止內(nèi)存泄漏 public void clear() { userHolder.remove(); // 清除當(dāng)前線程中的值 } }
2.使用 UserContext
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { @Autowired private UserContext userContext; public void login(String username) { userContext.setCurrentUser(username); System.out.println("User logged in: " + userContext.getCurrentUser()); } public void logout() { System.out.println("User logged out: " + userContext.getCurrentUser()); userContext.clear(); } }
3 示例測試類
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class UserServiceTest { @Autowired private UserService userService; @Test public void testThreadLocal() { userService.login("Alice"); userService.logout(); // Clear (will just have no output, but it demonstrates functionality) userService.login("Bob"); userService.logout(); } }
4. 圖形展示
在多線程環(huán)境中的 ThreadLocal 可能如下圖所示:
+-------------------+ | UserContext | |-------------------| | ThreadLocal | | - userHolder | +-------------------+ | | | | v v +------------+ +-------------+ | Thread A | | Thread B | |------------| |------------ | | - user: "Alice" | - user: "Bob" | +------------+ +--------------+
在每個線程中,UserContext 提供了對 ThreadLocal 變量獨(dú)立的值,使得 Thread A 可以存儲與 Thread B 不同的用戶會話信息。
5、解決方案
根據(jù)以上介紹Spring Bean的線程安全問題,以下是各種常用的解決方案。
1、同步機(jī)制去處理
synchronized 關(guān)鍵字或者 ReentrantLock 可重入鎖。
示例:
synchronized介紹:
import org.springframework.stereotype.Component; ? @Component public class OrderServiceBean { ? private int orderStatus; ? public synchronized void updateOrderStatus() { // 這里進(jìn)行更新訂單狀態(tài)的具體業(yè)務(wù)邏輯,比如根據(jù)某些條件修改orderStatus的值 orderStatus++; } ? public int getOrderStatus() { return orderStatus; } }
ReentrantLock介紹:
import org.springframework.stereotype.Component; import java.util.concurrent.locks.ReentrantLock; ? @Component public class OrderServiceBean { ? private int orderStatus; private ReentrantLock lock = new ReentrantLock(); ? public void updateOrderStatus() { lock.lock(); try { // 這里進(jìn)行更新訂單狀態(tài)的具體業(yè)務(wù)邏輯,比如根據(jù)某些條件修改orderStatus的值 orderStatus++; } finally { lock.unlock(); } } ? public int getOrderStatus() { return orderStatus; } }
2、Treadlocal對象(推薦)
3、采用不可變對象(Immutable Objects)
設(shè)置final對象或者成員變量屬性。
4、使用原子類(Atomic Classes)
import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicInteger; ? @Component public class VisitCountBean { ? private AtomicInteger visitCount = new AtomicInteger(0); ? public void incrementVisitCount() { visitCount.incrementAndGet(); } ? public int getVisitCount() { return visitCount.get(); } }
在 Spring 中實(shí)現(xiàn)線程安全,尤其是涉及到多個線程共享狀態(tài)時,常常需要:
- 選擇適當(dāng)?shù)?Bean 作用域。
- 盡量減少或避免可變狀態(tài)。
- 使用 ThreadLocal 來管理線程局部數(shù)據(jù)。
- 使用 AOP 及 Spring 事務(wù)來處理業(yè)務(wù)邏輯。
- 實(shí)現(xiàn)良好的設(shè)計(jì)模式以確保代碼的可維護(hù)性。
通過以上最佳實(shí)踐,可以有效地在 Spring 應(yīng)用中實(shí)現(xiàn)線程安全,確保系統(tǒng)的穩(wěn)定性和數(shù)據(jù)一致性。
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot整合Redis正確的實(shí)現(xiàn)分布式鎖的示例代碼
這篇文章主要介紹了SpringBoot整合Redis正確的實(shí)現(xiàn)分布式鎖的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07SpringBoot整合JWT(JSON?Web?Token)生成token與驗(yàn)證的流程及示例
JSON Web Token(JWT)是一種開放的標(biāo)準(zhǔn)(RFC 7519),定義了一種緊湊的、自包含的方式來安全地在各方之間傳輸信息作為JSON對象,這篇文章主要給大家介紹了關(guān)于SpringBoot整合JWT(JSON?Web?Token)生成token與驗(yàn)證的相關(guān)資料,需要的朋友可以參考下2024-07-07Java實(shí)現(xiàn)LRU緩存的實(shí)例詳解
這篇文章主要介紹了Java實(shí)現(xiàn)LRU緩存的實(shí)例詳解的相關(guān)資料,這里提供實(shí)例幫助大家理解掌握這部分內(nèi)容,需要的朋友可以參考下2017-08-08Java中實(shí)現(xiàn)WebSocket方法詳解
這篇文章主要介紹了Java中實(shí)現(xiàn)WebSocket方法詳解,WebSocket?是一種新型的網(wǎng)絡(luò)協(xié)議,它允許客戶端和服務(wù)器之間進(jìn)行雙向通信,可以實(shí)現(xiàn)實(shí)時數(shù)據(jù)交互,需要的朋友可以參考下2023-07-07java動態(tài)目錄樹的實(shí)現(xiàn)示例
在開發(fā)過程中,常常需要對目錄結(jié)構(gòu)進(jìn)行操作和展示,本文主要介紹了java動態(tài)目錄樹的實(shí)現(xiàn)示例,具有一定的參考價值,感興趣的可以了解一下2024-03-03SLF4J報錯解決:No SLF4J providers were found的
這篇文章主要介紹了SLF4J報錯解決:No SLF4J providers were found的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06Java實(shí)現(xiàn)根據(jù)模板自動生成新的PPT
這篇文章主要介紹了如何利用Java代碼自動生成PPT,具體就是查詢數(shù)據(jù)庫數(shù)據(jù),然后根據(jù)模板文件(PPT),將數(shù)據(jù)庫數(shù)據(jù)與模板文件(PPT),進(jìn)行組合一下,生成新的PPT文件。感興趣的可以了解一下2022-02-02