面試官:?jiǎn)卫鼴ean一定不安全嗎?實(shí)際工作中如何處理此問(wèn)題?
默認(rèn)情況下,Spring Boot 中的 Bean 是非線程安全的。這是因?yàn)?,默認(rèn)情況下 Bean 的作用域是單例模式,那么此時(shí),所有的請(qǐng)求都會(huì)共享同一個(gè) Bean 實(shí)例,這意味著這個(gè) Bean 實(shí)例,在多線程下可能被同時(shí)修改,那么此時(shí)它就會(huì)出現(xiàn)線程安全問(wèn)題。
“
Bean 的作用域(Scope)指的是確定在應(yīng)用程序中創(chuàng)建和管理 Bean 實(shí)例的范圍。也就是在 Spring 中,可以通過(guò)指定不同的作用域來(lái)控制 Bean 實(shí)例的生命周期和可見(jiàn)性。例如,單例模式就是所有線程可見(jiàn)并共享的,而原型模式則是每次請(qǐng)求都創(chuàng)建一個(gè)新的原型對(duì)象。
”
1、單例Bean一定不安全嗎?
并不是,單例 Bean 分為以下兩種類型:
- 無(wú)狀態(tài) Bean(線程安全):Bean 沒(méi)有成員變量,或多線程只會(huì)對(duì) Bean 成員變量進(jìn)行查詢操作,不會(huì)修改操作。
- 有狀態(tài) Bean(非線程安全):Bean 有成員變量,并且并發(fā)線程會(huì)對(duì)成員變量進(jìn)行修改操作。
所以說(shuō):有狀態(tài)的單例 Bean 是非線程安全的,而無(wú)狀態(tài)的 Bean 是線程安全的。
“
但在程序中,只要有一種情況會(huì)出現(xiàn)線程安全問(wèn)題,那么它的整體就是非線程安全的,所以總的來(lái)說(shuō),單例 Bean 還是非線程安全的。
”
(1)無(wú)狀態(tài)的Bean
無(wú)狀態(tài)的 Bean 指的是不存在成員變量,或只有查詢操作,沒(méi)有修改操作,它的實(shí)現(xiàn)示例代碼如下:
import org.springframework.stereotype.Service;
@Service
public class StatelessService {
public void doSomeTask() {
// 執(zhí)行任務(wù)
}
}
(2)有狀態(tài)的Bean
有成員變量,并且存在對(duì)成員變量的修改操作,如下代碼所示:
import org.springframework.stereotype.Service;
@Service
public class UserService {
private int count = 0;
public void incrementCount() {
count++; // 非原子操作,并發(fā)存在線程安全問(wèn)題
}
public int getCount() {
return count;
}
}
2、如何保證線程安全?
想要保證有狀態(tài) Bean 的線程安全,可以從以下幾個(gè)方面來(lái)實(shí)現(xiàn):
- 使用 ThreadLocal(線程本地變量):每個(gè)線程修改自己的變量,就沒(méi)有線程安全問(wèn)題了。
- 使用鎖機(jī)制:例如 synchronized 或 ReentrantLock 加鎖修改操作,保證線程安全。
- 設(shè)置 Bean 為原型作用域(Prototype):將 Bean 的作用域設(shè)置為原型,這意味著每次請(qǐng)求該 Bean 時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例,這樣可以防止不同線程之間的數(shù)據(jù)沖突,不過(guò)這種方法增加了內(nèi)存消耗。
- 使用線程安全容器:例如使用 Atomic 家族下的類(如 AtomicInteger)來(lái)保證線程安全,此實(shí)現(xiàn)方式的本質(zhì)還是通過(guò)鎖機(jī)制來(lái)保證線程安全的,Atomic 家族底層是通過(guò)樂(lè)觀鎖 CAS(Compare And Swap,比較并替換)來(lái)保證線程安全的。
具體實(shí)現(xiàn)如下。
(1)使用ThreadLocal保證線程安全
實(shí)現(xiàn)代碼如下:
import org.springframework.stereotype.Service;
@Service
public class UserService {
private ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> 0);
public void incrementCount() {
count.set(count.get() + 1);
}
public int getCount() {
return count.get();
}
}
使用 ThreadLocal 需要注意一個(gè)問(wèn)題,在用完之后記得調(diào)用 ThreadLocal 的 remove 方法,不然會(huì)發(fā)生內(nèi)存泄漏問(wèn)題。
(2)使用鎖機(jī)制
鎖機(jī)制中最簡(jiǎn)單的是使用 synchronized 修飾方法,讓多線程執(zhí)行此方法時(shí)排隊(duì)執(zhí)行,這樣就不會(huì)有線程安全問(wèn)題了,如下代碼所示:
import org.springframework.stereotype.Service;
@Service
public class UserService {
private int count = 0;
public synchronized void incrementCount() {
count++; // 非原子操作,并發(fā)存在線程安全問(wèn)題
}
public int getCount() {
return count;
}
}
(3)設(shè)置為原型作用域
原型作用域通過(guò) @Scope("prototype") 來(lái)設(shè)置,表示每次請(qǐng)求時(shí)都會(huì)生成一個(gè)新對(duì)象(也就沒(méi)有線程安全問(wèn)題了),如下代碼所示:
import org.springframework.stereotype.Service;
@Service
@Scope("prototype")
public class UserService {
private int count = 0;
public void incrementCount() {
count++; // 非原子操作,并發(fā)存在線程安全問(wèn)題
}
public int getCount() {
return count;
}
}
(4)使用線程安全容器
我們可以使用線程安全的容器,例如 AtomicInteger 來(lái)替代 int,從而保證線程安全,如下代碼所示:
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class UserService {
private AtomicInteger count = new AtomicInteger(0);
public void incrementCount() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
實(shí)際工作中如何保證線程安全?
實(shí)際工作中,通常會(huì)根據(jù)具體的業(yè)務(wù)場(chǎng)景來(lái)選擇合適的線程安全方案,但是以上解決線程安全的方案中,ThreadLocal 和原型作用域會(huì)使用更多的資源,占用更多的空間來(lái)保證線程安全,所以在使用時(shí)通常不會(huì)作為最佳考慮方案。
而鎖機(jī)制和線程安全的容器通常會(huì)優(yōu)先考慮,但需要注意的是 AtomicInteger 底層是樂(lè)觀鎖 CAS 實(shí)現(xiàn)的,因此它存在樂(lè)觀鎖的典型問(wèn)題 ABA 問(wèn)題(如果有狀態(tài)的 Bean 中既有 ++ 操作,又有 -- 操作時(shí),可能會(huì)出現(xiàn) ABA 問(wèn)題),此時(shí)就要使用鎖機(jī)制,或 AtomicStampedReference 來(lái)解決 ABA 問(wèn)題了。
小結(jié)
單例模式的 Bean 并不一定都是非線程安全的,其中有狀態(tài)的 Bean 是存在線程安全問(wèn)題的。實(shí)際工作中通常會(huì)使用鎖機(jī)制(synchronized 或 ReentrantLock)或線程安全的容器來(lái)解決 Bean 的線程安全問(wèn)題,但具體使用哪種方案,還要結(jié)合具體業(yè)務(wù)場(chǎng)景來(lái)定。