阿里一面:Spring Bean 默認(rèn)是單例的,高并發(fā)情況下,如何保證并發(fā)...
Spring的bean默認(rèn)都是單例的,某些情況下,單例是并發(fā)不安全的,以Controller舉例,問(wèn)題根源在于,我們可能會(huì)在Controller中定義成員變量,如此一來(lái),多個(gè)請(qǐng)求來(lái)臨,進(jìn)入的都是同一個(gè)單例的Controller對(duì)象,并對(duì)此成員變量的值進(jìn)行修改操作,因此會(huì)互相影響,無(wú)法達(dá)到并發(fā)安全(不同于線程隔離的概念,后面會(huì)解釋到)的效果。
1、拋出問(wèn)題
首先來(lái)舉個(gè)例子,證明單例的并發(fā)不安全性:
@Controller
public?class?HomeController?{
????private?int?i;
????@GetMapping("testsingleton1")
????@ResponseBody
????public?int?test1()?{
????????return?++i;
????}
}
多次訪問(wèn)此url,可以看到每次的結(jié)果都是自增的,所以這樣的代碼顯然是并發(fā)不安全的。
2、解決方案
因此,我們?yōu)榱俗専o(wú)狀態(tài)的海量Http請(qǐng)求之間不受影響,我們可以采取以下幾種措施:
2.1 單例變?cè)?/span>
對(duì)web項(xiàng)目,可以Controller類(lèi)上加注解@Scope("prototype")或@Scope("request"),對(duì)非web項(xiàng)目,在Component類(lèi)上添加注解@Scope("prototype")。
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單;
缺點(diǎn):很大程度上增大了bean創(chuàng)建實(shí)例化銷(xiāo)毀的服務(wù)器資源開(kāi)銷(xiāo)。
2.2 線程隔離類(lèi)ThreadLocal
有人想到了線程隔離類(lèi)ThreadLocal,我們嘗試將成員變量包裝為T(mén)hreadLocal,以試圖達(dá)到并發(fā)安全,同時(shí)打印出Http請(qǐng)求的線程名,修改代碼如下:
@Controller
public?class?HomeController?{
????private?ThreadLocal<Integer>?i?=?new?ThreadLocal<>();
????@GetMapping("testsingleton1")
????@ResponseBody
????public?int?test1()?{
????????if?(i.get()?==?null)?{
????????????i.set(0);
????????}
????????i.set(i.get().intValue()?+?1);
????????log.info("{}?->?{}",?Thread.currentThread().getName(),?i.get());
????????return?i.get().intValue();
????}
}
多次訪問(wèn)此url測(cè)試一把,打印日志如下:
[INFO?]?2019-12-03?11:49:08,226?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-1?->?1
[INFO?]?2019-12-03?11:49:16,457?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-2?->?1
[INFO?]?2019-12-03?11:49:17,858?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-3?->?1
[INFO?]?2019-12-03?11:49:18,461?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-4?->?1
[INFO?]?2019-12-03?11:49:18,974?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-5?->?1
[INFO?]?2019-12-03?11:49:19,696?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-6?->?1
[INFO?]?2019-12-03?11:49:22,138?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-7?->?1
[INFO?]?2019-12-03?11:49:22,869?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-9?->?1
[INFO?]?2019-12-03?11:49:23,617?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-8?->?1
[INFO?]?2019-12-03?11:49:24,569?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-10?->?1
[INFO?]?2019-12-03?11:49:25,218?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-1?->?2
[INFO?]?2019-12-03?11:49:25,740?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-2?->?2
[INFO?]?2019-12-03?11:49:43,308?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-3?->?2
[INFO?]?2019-12-03?11:49:44,420?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-4?->?2
[INFO?]?2019-12-03?11:49:45,271?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-5?->?2
[INFO?]?2019-12-03?11:49:45,808?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-6?->?2
[INFO?]?2019-12-03?11:49:46,272?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-7?->?2
[INFO?]?2019-12-03?11:49:46,489?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-9?->?2
[INFO?]?2019-12-03?11:49:46,660?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-8?->?2
[INFO?]?2019-12-03?11:49:46,820?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-10?->?2
[INFO?]?2019-12-03?11:49:46,990?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-1?->?3
[INFO?]?2019-12-03?11:49:47,163?com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-2?->?3
......
從日志分析出,二十多次的連續(xù)請(qǐng)求得到的結(jié)果有1有2有3等等,而我們期望不管我并發(fā)請(qǐng)求有多少,每次的結(jié)果都是1;同時(shí)可以發(fā)現(xiàn)web服務(wù)器默認(rèn)的請(qǐng)求線程池大小為10,這10個(gè)核心線程可以被之后不同的Http請(qǐng)求復(fù)用,所以這也是為什么相同線程名的結(jié)果不會(huì)重復(fù)的原因。
“總結(jié):ThreadLocal的方式可以達(dá)到線程隔離,但還是無(wú)法達(dá)到并發(fā)安全。
”
2.3 盡量避免使用成員變量
有人說(shuō),單例bean的成員變量這么麻煩,能不用成員變量就盡量避免這么用,在業(yè)務(wù)允許的條件下,將成員變量替換為RequestMapping方法中的局部變量,多省事。這種方式自然是最恰當(dāng)?shù)模救艘彩亲钔扑]。代碼修改如下:
@Controller
public?class?HomeController?{
????@GetMapping("testsingleton1")
????@ResponseBody
????public?int?test1()?{
?????????int?i?=?0;
?????????//?TODO?biz?code
?????????return?++i;
????}
}
但當(dāng)很少的某種情況下,必須使用成員變量呢,我們?cè)撛趺刺幚恚?/p>
2.4 使用并發(fā)安全的類(lèi)
Java作為功能性超強(qiáng)的編程語(yǔ)言,API豐富,如果非要在單例bean中使用成員變量,可以考慮使用并發(fā)安全的容器,如ConcurrentHashMap、ConcurrentHashSet等等等等,將我們的成員變量(一般可以是當(dāng)前運(yùn)行中的任務(wù)列表等這類(lèi)變量)包裝到這些并發(fā)安全的容器中進(jìn)行管理即可。
2.5 分布式或微服務(wù)的并發(fā)安全
如果還要進(jìn)一步考慮到微服務(wù)或分布式服務(wù)的影響,方式4便不足以處理了,所以可以借助于可以共享某些信息的分布式緩存中間件如Redis等,這樣即可保證同一種服務(wù)的不同服務(wù)實(shí)例都擁有同一份共享信息(如當(dāng)前運(yùn)行中的任務(wù)列表等這類(lèi)變量)。
3、補(bǔ)充說(shuō)明
spring bean作用域有以下5個(gè):
-
singleton:?jiǎn)卫J剑?dāng)spring創(chuàng)建applicationContext容器的時(shí)候,spring會(huì)欲初始化所有的該作用域?qū)嵗由蟣azy-init就可以避免預(yù)處理; -
prototype:原型模式,每次通過(guò)getBean獲取該bean就會(huì)新產(chǎn)生一個(gè)實(shí)例,創(chuàng)建后spring將不再對(duì)其管理;
(下面是在web項(xiàng)目下才用到的)
-
request:搞web的大家都應(yīng)該明白request的域了吧,就是每次請(qǐng)求都新產(chǎn)生一個(gè)實(shí)例,和prototype不同就是創(chuàng)建后,接下來(lái)的管理,spring依然在監(jiān)聽(tīng); -
session:每次會(huì)話,同上; -
global session:全局的web域,類(lèi)似于servlet中的application。
