
作者:DayDayUp丶
來源:blog.csdn.net/songzehao/article/details/103365494
首先來舉個例子,證明單例的並發不安全性:
@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個:
(下面是在web項目下才用到的)
global session:全局的web域,類似於servlet中的application。
END
關注後端面試那些事,回復【2022面經】
獲取最新大廠Java面經

