close
關注我,回復關鍵字「spring」,
免費領取Spring學習資料。

作者:DayDayUp丶

來源:blog.csdn.net/songzehao/article/details/103365494


Spring的bean默認都是單例的,某些情況下,單例是並發不安全的,以Controller舉例,問題根源在於,我們可能會在Controller中定義成員變量,如此一來,多個請求來臨,進入的都是同一個單例的Controller對象,並對此成員變量的值進行修改操作,因此會互相影響,無法達到並發安全(不同於線程隔離的概念,後面會解釋到)的效果。
1一、拋出問題

首先來舉個例子,證明單例的並發不安全性:

@ControllerpublicclassHomeController{privateinti;@GetMapping("testsingleton1")@ResponseBodypublicinttest1(){return++i;}}

多次訪問此url,可以看到每次的結果都是自增的,所以這樣的代碼顯然是並發不安全的。

2二、解決方案

因此,我們為了讓無狀態的海量Http請求之間不受影響,我們可以採取以下幾種措施:

2.1 單例變原型

對web項目,可以Controller類上加註解@Scope("prototype")或@Scope("request"),對非web項目,在Component類上添加註解@Scope("prototype")。

優點:實現簡單;

缺點:很大程度上增大了bean創建實例化銷毀的服務器資源開銷。

2.2 線程隔離類ThreadLocal

有人想到了線程隔離類ThreadLocal,我們嘗試將成員變量包裝為ThreadLocal,以試圖達到並發安全,同時打印出Http請求的線程名,修改代碼如下:

@ControllerpublicclassHomeController{privateThreadLocal<Integer>i=newThreadLocal<>();@GetMapping("testsingleton1")@ResponseBodypublicinttest1(){if(i.get()==null){i.set(0);}i.set(i.get().intValue()+1);log.info("{}->{}",Thread.currentThread().getName(),i.get());returni.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......

從日誌分析出,二十多次的連續請求得到的結果有1有2有3等等,而我們期望不管我並發請求有多少,每次的結果都是1;同時可以發現web服務器默認的請求線程池大小為10,這10個核心線程可以被之後不同的Http請求復用,所以這也是為什麼相同線程名的結果不會重複的原因。

總結:ThreadLocal的方式可以達到線程隔離,但還是無法達到並發安全。

2.3 儘量避免使用成員變量

有人說,單例bean的成員變量這麼麻煩,能不用成員變量就儘量避免這麼用,在業務允許的條件下,將成員變量替換為RequestMapping方法中的局部變量,多省事。這種方式自然是最恰當的,本人也是最推薦。代碼修改如下:

@ControllerpublicclassHomeController{@GetMapping("testsingleton1")@ResponseBodypublicinttest1(){inti=0;//TODObizcodereturn++i;}}

但當很少的某種情況下,必須使用成員變量呢,我們該怎麼處理?

2.4 使用並發安全的類

Java作為功能性超強的編程語言,API豐富,如果非要在單例bean中使用成員變量,可以考慮使用並發安全的容器,如ConcurrentHashMap、ConcurrentHashSet等等等等,將我們的成員變量(一般可以是當前運行中的任務列表等這類變量)包裝到這些並發安全的容器中進行管理即可。

2.5 分布式或微服務的並發安全

如果還要進一步考慮到微服務或分布式服務的影響,方式4便不足以處理了,所以可以藉助於可以共享某些信息的分布式緩存中間件如Redis等,這樣即可保證同一種服務的不同服務實例都擁有同一份共享信息(如當前運行中的任務列表等這類變量)。另外,歡迎關注公眾號後端面試那些事,回覆:簡歷,即可免費獲取優質簡歷模板。

3三、補充說明

spring bean作用域有以下5個:

singleton:單例模式,當spring創建applicationContext容器的時候,spring會欲初始化所有的該作用域實例,加上lazy-init就可以避免預處理;
prototype:原型模式,每次通過getBean獲取該bean就會新產生一個實例,創建後spring將不再對其管理;

(下面是在web項目下才用到的)

request:搞web的大家都應該明白request的域了吧,就是每次請求都新產生一個實例,和prototype不同就是創建後,接下來的管理,spring依然在監聽;
session:每次會話,同上;

global session:全局的web域,類似於servlet中的application。


END


現在可以為 Spring Boot 3.0 做哪些準備?
Spring Boot 使用 Elastic Job 實現定時任務
推薦一個好看的IDEA主題和圖標集
Spring Boot中使用PostgreSQL數據庫
Spring Cloud Gateway CORS方案看這篇就夠了

關注後端面試那些事,回復【2022面經】

獲取最新大廠Java面經


最後重要提示:高質量的技術交流群,限時免費開放,今年抱團最重要。想進群的,關注SpringForAll社區,回復關鍵詞:加群,拉你進群。




點擊「閱讀原文」領取2022大廠面經
↓↓↓
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()