Spring Bean默認是單例的,高并發(fā)情況下,如何保證并發(fā)安全?
點擊關注公眾號,Java干貨及時送達
作者:DayDayUp丶
來源:blog.csdn.net/songzehao/article/details/103365494
1一、拋出問題
首先來舉個例子,證明單例的并發(fā)不安全性:
@Controller
public?class?HomeController?{
????private?int?i;
????@GetMapping("testsingleton1")
????@ResponseBody
????public?int?test1()?{
????????return?++i;
????}
}
多次訪問此url,可以看到每次的結果都是自增的,所以這樣的代碼顯然是并發(fā)不安全的。
2二、解決方案
因此,我們?yōu)榱俗専o狀態(tài)的海量Http請求之間不受影響,我們可以采取以下幾種措施:
2.1 單例變原型
對web項目,可以Controller類上加注解@Scope("prototype")或@Scope("request"),對非web項目,在Component類上添加注解@Scope("prototype")。
優(yōu)點:實現(xiàn)簡單;
缺點:很大程度上增大了bean創(chuàng)建實例化銷毀的服務器資源開銷。
2.2 線程隔離類ThreadLocal
有人想到了線程隔離類ThreadLocal,我們嘗試將成員變量包裝為ThreadLocal,以試圖達到并發(fā)安全,同時打印出Http請求的線程名,修改代碼如下:
@Controller
public?class?HomeController?{
????private?ThreadLocal?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();
????}
}
多次訪問此url測試一把,打印日志如下:
[INFO ] 2021-12-03 11:49:08,226 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-1 -> 1
[INFO ] 2021-12-03 11:49:16,457 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-2 -> 1
[INFO ] 2021-12-03 11:49:17,858 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-3 -> 1
[INFO ] 2021-12-03 11:49:18,461 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-4 -> 1
[INFO ] 2021-12-03 11:49:18,974 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-5 -> 1
[INFO ] 2021-12-03 11:49:19,696 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-6 -> 1
[INFO ] 2021-12-03 11:49:22,138 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-7 -> 1
[INFO ] 2021-12-03 11:49:22,869 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-9 -> 1
[INFO ] 2021-12-03 11:49:23,617 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-8 -> 1
[INFO ] 2021-12-03 11:49:24,569 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-10 -> 1
[INFO ] 2021-12-03 11:49:25,218 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-1 -> 2
[INFO ] 2021-12-03 11:49:25,740 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-2 -> 2
[INFO ] 2021-12-03 11:49:43,308 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-3 -> 2
[INFO ] 2021-12-03 11:49:44,420 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-4 -> 2
[INFO ] 2021-12-03 11:49:45,271 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-5 -> 2
[INFO ] 2021-12-03 11:49:45,808 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-6 -> 2
[INFO ] 2021-12-03 11:49:46,272 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-7 -> 2
[INFO ] 2021-12-03 11:49:46,489 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-9 -> 2
[INFO ] 2021-12-03 11:49:46,660 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-8 -> 2
[INFO ] 2021-12-03 11:49:46,820 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-10 -> 2
[INFO ] 2021-12-03 11:49:46,990 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-1 -> 3
[INFO ] 2021-12-03 11:49:47,163 com.cjia.ds.controller.HomeController.test1(HomeController.java:50)
http-nio-8080-exec-2 -> 3
......
從日志分析出,二十多次的連續(xù)請求得到的結果有1有2有3等等,而我們期望不管我并發(fā)請求有多少,每次的結果都是1;同時可以發(fā)現(xiàn)web服務器默認的請求線程池大小為10,這10個核心線程可以被之后不同的Http請求復用,所以這也是為什么相同線程名的結果不會重復的原因。
總結:ThreadLocal的方式可以達到線程隔離,但還是無法達到并發(fā)安全。
2.3 盡量避免使用成員變量
有人說,單例bean的成員變量這么麻煩,能不用成員變量就盡量避免這么用,在業(yè)務允許的條件下,將成員變量替換為RequestMapping方法中的局部變量,多省事。這種方式自然是最恰當?shù)模救艘彩亲钔扑]。代碼修改如下:
@Controller
public?class?HomeController?{
????@GetMapping("testsingleton1")
????@ResponseBody
????public?int?test1()?{
?????????int?i?=?0;
?????????//?TODO?biz?code
?????????return?++i;
????}
}
但當很少的某種情況下,必須使用成員變量呢,我們該怎么處理?
2.4 使用并發(fā)安全的類
Java作為功能性超強的編程語言,API豐富,如果非要在單例bean中使用成員變量,可以考慮使用并發(fā)安全的容器,如ConcurrentHashMap、ConcurrentHashSet等等等等,將我們的成員變量(一般可以是當前運行中的任務列表等這類變量)包裝到這些并發(fā)安全的容器中進行管理即可。
2.5 分布式或微服務的并發(fā)安全
如果還要進一步考慮到微服務或分布式服務的影響,方式4便不足以處理了,所以可以借助于可以共享某些信息的分布式緩存中間件如Redis等,這樣即可保證同一種服務的不同服務實例都擁有同一份共享信息(如當前運行中的任務列表等這類變量)。另外,歡迎關注公眾號后端面試那些事,回復:簡歷,即可免費獲取優(yōu)質簡歷模板。
3三、補充說明
spring bean作用域有以下5個:
singleton:單例模式,當spring創(chuàng)建applicationContext容器的時候,spring會欲初始化所有的該作用域實例,加上lazy-init就可以避免預處理;prototype:原型模式,每次通過getBean獲取該bean就會新產生一個實例,創(chuàng)建后spring將不再對其管理;
(下面是在web項目下才用到的)
request:搞web的大家都應該明白request的域了吧,就是每次請求都新產生一個實例,和prototype不同就是創(chuàng)建后,接下來的管理,spring依然在監(jiān)聽;session:每次會話,同上;global session:全局的web域,類似于servlet中的application。
? ? ?
往 期 推 薦
點分享
點收藏
點點贊
點在看





