對(duì)Spring中bean線程安全的討論
Spring容器中的Bean是否線程安全,容器本身并沒有提供Bean的線程安全策略,因此Spring容器中的Bean本身不具備線程安全的特性,但是具體要結(jié)合具體的scope、靜態(tài)變量、常量、成員變量等多種屬性去研究。
1、Bean狀態(tài)介紹
1.1、有狀態(tài)對(duì)象
有實(shí)例變量的對(duì)象,即每個(gè)用戶最初都會(huì)得到一個(gè)初始的bean,可以保存數(shù)據(jù),是非線程安全的。
每個(gè)用戶有自己特有的一個(gè)實(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;
}
}
- 多個(gè)線程調(diào)用 increment() 時(shí),count++操作可能因指令重排或并發(fā)寫入導(dǎo)致數(shù)據(jù)不一致。
1.2、無狀態(tài)對(duì)象
沒有實(shí)例變量的對(duì)象,不能保存數(shù)據(jù),是不變類,是線程安全的。
- bean一旦實(shí)例化就被加進(jìn)會(huì)話池中,各個(gè)用戶都可以共用。即使用戶已經(jīng)消亡,bean 的生命期也不一定結(jié)束,它可能依然存在于會(huì)話池中,供其他用戶調(diào)用。
- 由于沒有特定的用戶,那么也就不能保持某一用戶的狀態(tài),所以叫無狀態(tài)bean。但無狀態(tài)會(huì)話bean 并非沒有狀態(tài),
- 如果有自己的屬性(變量),那么這些變量就會(huì)受到所有調(diào)用它的用戶的影響。
代碼示例如下:
@Service
public class Calculator {
// 無狀態(tài):不保存任何實(shí)例變量
public int add(int a, int b) {
return a + b;
}
}
- 多個(gè)線程調(diào)用add(1,2)時(shí),結(jié)果不會(huì)互相影響。
兩者的區(qū)別和聯(lián)系:

2、Bean作用域
bean的生命周期如下所示:
實(shí)例化--->設(shè)置屬性--->初始化--->銷毀

Spring 的 bean 作用域(scope)類型:
1、singleton:單例,默認(rèn)作用域。
- 優(yōu)點(diǎn): 節(jié)省內(nèi)存,因?yàn)橹淮嬖谝粋€(gè)實(shí)例。
- 缺點(diǎn): 由于多個(gè)線程可能共享同一個(gè)實(shí)例,需要格外注意線程安全(非線程安全的狀態(tài)字段可能導(dǎo)致問題)
2、prototype:原型,每次創(chuàng)建一個(gè)新對(duì)象。
3、request:請(qǐng)求,每次Http請(qǐng)求創(chuàng)建一個(gè)新對(duì)象,適用于WebApplicationContext環(huán)境下。
4、session:會(huì)話,同一個(gè)會(huì)話共享一個(gè)實(shí)例,不同會(huì)話使用不用的實(shí)例。
5、global-session:全局會(huì)話,所有會(huì)話共享一個(gè)實(shí)例。
3、線程安全:
從單例與原型Bean分別進(jìn)行說明。

3.1、bean的分類
1、原型Bean
對(duì)于原型Bean,每次創(chuàng)建一個(gè)新對(duì)象,也就是線程之間并不存在Bean共享,自然是不會(huì)有線程安全的問題。
2、單例Bean
對(duì)于單例Bean,所有線程都共享一個(gè)單例實(shí)例Bean,因此是存在資源的競(jìng)爭(zhēng)。
3.2、bean的安全
1、@Controller相關(guān)
可以這樣理解:
如果單例Bean,是一個(gè)無狀態(tài)Bean,在線程中的操作不會(huì)對(duì)Bean的成員執(zhí)行查詢以外的操作,那么這個(gè)單例Bean是線程安全的。
比如Spring mvc 的 Controller、Service、Dao等,這些Bean大多是無狀態(tài)的,默認(rèn)情況下@Controller沒有加上@Scope,默認(rèn)Scope就是默認(rèn)值singleton,單例的 ,系統(tǒng)只會(huì)初始化一次 Controller 容器,只關(guān)注于方法本身。
但是,如果每次請(qǐng)求的都是同一個(gè) 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
每個(gè)請(qǐng)求都單獨(dú)創(chuàng)建一個(gè) Controller 容器,所以各個(gè)請(qǐng)求之間是線程安全的。
@RestController
@Scope(value = "prototype") // 加上@Scope注解,有2個(gè)取值:?jiǎn)卫?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對(duì)象,在不設(shè)置scope的時(shí)候也是線程不安全的。
4、一定要定義變量的話,用 ThreadLocal 來封裝,這個(gè)是線程安全的。
2、@prototype注解
@Scope 注解的 prototype 實(shí)例一定就是線程安全的嗎?
答案是否定的。上面已經(jīng)解釋過了,需要根據(jù)多方位去考量。
@RestController
@Scope(value = "prototype") // 加上@Scope注解,有2個(gè)取值:?jiǎn)卫?singleton 多實(shí)例-prototype
public class TestController {
private int var = 0;
//只會(huì)初始化一次,因此也非線程安全的變量
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; // 定義一個(gè)普通變量
?
private static int staticVar = 0; // 定義一個(gè)靜態(tài)變量
?
@Value("${test-int}")
private int testInt; // 從配置文件中讀取變量
?
ThreadLocal<Integer> tl = new ThreadLocal<>(); // 用ThreadLocal來封裝變量
?
@Autowired
private User user; // 注入一個(gè)對(duì)象來封裝變量
?
@GetMapping(value = "/test_var")
public String test() {
tl.set(1);
System.out.println("先取一下user對(duì)象中的值:"+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對(duì)象中的值:0===再取一下hashCode:241165852
普通變量var:1===靜態(tài)變量staticVar:1===配置變量testInt:1===ThreadLocal變量tl:1===注入變量user:1
先取一下user對(duì)象中的值:1===再取一下hashCode:241165852
普通變量var:2===靜態(tài)變量staticVar:2===配置變量testInt:2===ThreadLocal變量tl:1===注入變量user:1
先取一下user對(duì)象中的值:1===再取一下hashCode:241165852
普通變量var:3===靜態(tài)變量staticVar:3===配置變量testInt:3===ThreadLocal變量tl:1===注入變量user:1
在單例模式下 Controller 中只有用 ThreadLocal 封裝的變量是線程安全的??梢钥吹?次請(qǐng)求結(jié)果里面只有 ThreadLocal 變量值每次都是從 0+1=1 的,其他的幾個(gè)都是累加的,而 user 對(duì)象呢,默認(rèn)值是0,第二交取值的時(shí)候就已經(jīng)是1了,關(guān)鍵它的 hashCode 是一樣的,說明每次請(qǐng)求調(diào)用的都是同一個(gè) user 對(duì)象。
TestController 上的 @Scope 注解的屬性改一下改成多實(shí)例的: @Scope(value = "prototype") ,其他都不變,再次請(qǐng)求,結(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; // 定義一個(gè)普通變量
?
private static int staticVar = 0; // 定義一個(gè)靜態(tài)變量
?
@Value("${test-int}")
private int testInt; // 從配置文件中讀取變量
?
ThreadLocal<Integer> tl = new ThreadLocal<>(); // 用ThreadLocal來封裝變量
?
@Autowired
private User user; // 注入一個(gè)對(duì)象來封裝變量
?
@GetMapping(value = "/test_var")
public String test() {
tl.set(1);
System.out.println("先取一下user對(duì)象中的值:"+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對(duì)象中的值:0===再取一下hashCode:1612967699
普通變量var:1===靜態(tài)變量staticVar:1===配置變量testInt:1===ThreadLocal變量tl:1===注入變量user:1先取一下user對(duì)象中的值:0===再取一下hashCode:985418837
普通變量var:1===靜態(tài)變量staticVar:2===配置變量testInt:1===ThreadLocal變量tl:1===注入變量user:1先取一下user對(duì)象中的值: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í)例(單例或原型)共享同一個(gè)靜態(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 對(duì)象中的變量都是非線程安全的。
4、ThreadLocal
4.1、概念
ThreadLocal 類提供了線程局部變量,每個(gè)線程可以將一個(gè)值存在 ThreadLocal 對(duì)象中,其他線程無法訪問這些值。每個(gè)線程都有自己獨(dú)立的變量副本。
ThreadLocal 的初始值可通過 withInitial() 方法設(shè)置:
private static final ThreadLocal<String> requestId =
ThreadLocal.withInitial(() -> "default-id");
簡(jiǎn)單的內(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)(如用戶請(qǐng)求上下文),多線程場(chǎng)景下,多個(gè)線程對(duì)這個(gè)單例Bean的成員變量并不存在資源的競(jìng)爭(zhēng),因?yàn)門hreadLocal為每個(gè)線程保存線程私有的數(shù)據(jù)。這是一種以空間換時(shí)間的方式。
4.3、原理
如下圖所示:

調(diào)用 ThreadLocal.set(value)方法時(shí),它會(huì)將這個(gè)值與當(dāng)前線程關(guān)聯(lián),而該值被存儲(chǔ)在當(dāng)前線程的一個(gè)內(nèi)部數(shù)據(jù)結(jié)構(gòu)中。通過 ThreadLocal.get()方法,可以獲取當(dāng)前線程所關(guān)聯(lián)的值。
- 核心機(jī)制:每個(gè)線程內(nèi)部維護(hù)一個(gè) ThreadLocalMap(類似鍵值對(duì)存儲(chǔ),以 ThreadLocal 對(duì)象為鍵,存儲(chǔ)線程私有的變量。
- 數(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ù)了多個(gè)entry。如下所示:

一個(gè)entry由key(threadlocal)和value,Entry繼承了弱引用,關(guān)于弱引用可參考:對(duì)Java 資源管理和引用體系的介紹
如下圖所示:entry

如果使用不當(dāng),會(huì)引發(fā)oom問題,主要是由GC回收機(jī)制和內(nèi)存結(jié)構(gòu)兩者引起。
可參考:就ThreadLocal使用時(shí)OOM的討論
4.5、使用場(chǎng)景
- 用戶會(huì)話信息: 在 web 應(yīng)用中維護(hù)用戶的會(huì)話信息,避免將狀態(tài)信息寫到全局上下文。
- 數(shù)據(jù)庫(kù)連接: 在線程中維護(hù)數(shù)據(jù)源連接,避免不同線程之間共享資源引起的競(jìng)爭(zhēng)。
- 事務(wù)管理(如 Spring 的 TransactionSynchronizationManager)。
以下是一個(gè)簡(jiǎn)單的 Spring Bean 示例,展示如何在 Spring 中使用 ThreadLocal 來存儲(chǔ)用戶會(huì)話信息。
1.定義一個(gè) 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 示例測(cè)試類
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" |
+------------+ +--------------+
在每個(gè)線程中,UserContext 提供了對(duì) ThreadLocal 變量獨(dú)立的值,使得 Thread A 可以存儲(chǔ)與 Thread B 不同的用戶會(huì)話信息。
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對(duì)象(推薦)
3、采用不可變對(duì)象(Immutable Objects)
設(shè)置final對(duì)象或者成員變量屬性。
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)線程安全,尤其是涉及到多個(gè)線程共享狀態(tài)時(shí),常常需要:
- 選擇適當(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é)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot整合Redis正確的實(shí)現(xiàn)分布式鎖的示例代碼
這篇文章主要介紹了SpringBoot整合Redis正確的實(shí)現(xiàn)分布式鎖的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
SpringBoot整合JWT(JSON?Web?Token)生成token與驗(yàn)證的流程及示例
JSON Web Token(JWT)是一種開放的標(biāo)準(zhǔn)(RFC 7519),定義了一種緊湊的、自包含的方式來安全地在各方之間傳輸信息作為JSON對(duì)象,這篇文章主要給大家介紹了關(guān)于SpringBoot整合JWT(JSON?Web?Token)生成token與驗(yàn)證的相關(guān)資料,需要的朋友可以參考下2024-07-07
Java實(shí)現(xiàn)LRU緩存的實(shí)例詳解
這篇文章主要介紹了Java實(shí)現(xiàn)LRU緩存的實(shí)例詳解的相關(guān)資料,這里提供實(shí)例幫助大家理解掌握這部分內(nèi)容,需要的朋友可以參考下2017-08-08
Java中實(shí)現(xiàn)WebSocket方法詳解
這篇文章主要介紹了Java中實(shí)現(xiàn)WebSocket方法詳解,WebSocket?是一種新型的網(wǎng)絡(luò)協(xié)議,它允許客戶端和服務(wù)器之間進(jìn)行雙向通信,可以實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)交互,需要的朋友可以參考下2023-07-07
java動(dòng)態(tài)目錄樹的實(shí)現(xiàn)示例
在開發(fā)過程中,常常需要對(duì)目錄結(jié)構(gòu)進(jìn)行操作和展示,本文主要介紹了java動(dòng)態(tài)目錄樹的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03
SLF4J報(bào)錯(cuò)解決:No SLF4J providers were found的
這篇文章主要介紹了SLF4J報(bào)錯(cuò)解決:No SLF4J providers were found的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
Java實(shí)現(xiàn)根據(jù)模板自動(dòng)生成新的PPT
這篇文章主要介紹了如何利用Java代碼自動(dòng)生成PPT,具體就是查詢數(shù)據(jù)庫(kù)數(shù)據(jù),然后根據(jù)模板文件(PPT),將數(shù)據(jù)庫(kù)數(shù)據(jù)與模板文件(PPT),進(jìn)行組合一下,生成新的PPT文件。感興趣的可以了解一下2022-02-02

