使用springboot單例模式與線程安全問題踩的坑
springboot單例模式與線程安全問題踩的坑
最近有客戶反映,使用公司產品時,偶爾會存在崩潰情況,自己測試無問題,然後去查日志,是報空指針。
於是順藤摸瓜 往上找,好嘛,之前的開發使用瞭成員變量,感覺問題就是在這裡瞭,因為眾所周知,springboot 采用的是單例模式,所以,使用成員變量時一定要謹慎。
下面上一張該類的截圖:
大傢可能看到瞭,該類上面加上瞭@Scope(“prototype”) 註解,該註解的作用是將該類變成多例模式。講道理因為變為瞭多例,應該不會有線程問題瞭。
我先說下我這邊的一個代碼環境,上面大傢看到的BaseController這個類裡面有個init方法,會在繼承它的類的所有方法前執行。
使用的是@ModelAttribute註解,這個註解的意思是,在該controller的所有方法前執行,意在初始化,我猜測之前的同事應該是為瞭獲取相同的一些參數,抽調出來做一個父類,隨著迭代,別的同事為瞭方便,拿來就用,導致很多controller繼承瞭該類。
@Scope(“prototype”)註解:
大傢設想一下,若父類加瞭@Scope(“prototype”)註解,子類controller並沒有加該註解,會怎樣呢?該註解是否還有意義?再比如,我在某service上加上@Scope(“prototype”)註解,但調用的controller沒有加@Scope(“prototype”)註解,那麼會出現什麼樣的結果呢?大傢可以去測試一下,測試方法也很簡單,就是在對應的父類或service的無參構造方法裡打印該類的地址。
下面說下我的測試結果:
先說父類上加瞭@Scope(“prototype”)註解,子類上沒有加這種情況。結果是,同一子類繼承的為同一父類,不同子類繼承為不同父類。理解一下,很簡單,因為springboot為單例模式,所以子類為單例,那麼隻有一個子類,父類肯定是一樣的。所以,不同線程過來使用的為同一變量,就會有問題。
同理:
在service上標註@Scope(“prototype”)註解,那在同一個controller裡,該service還是同一個,也就是說還是單例的,在不同的controller裡 是不同的。測試方法同上。
現在說下解決方法:
1、是在繼承該controller的子類上都加上@Scope(“prototype”)註解。這樣做的好處是簡單。壞處也同樣明顯,因為是多例的,那麼就會產生大量的實體類,占用大量內存,若是回收不及時,有可能會出現內存溢出。
2、是將變量私有化,比如使用線程變量,對變量加鎖等,技術上會復雜一些,而且調試不太好調試。說不定那些地方就會出現問題,畢竟是老代碼。
3、將該類轉換為攔截器,將變量放入request裡,用的時候取出來。
SpringMVC 或 SpringBoot 默認是單例模式(Singleton)
多個請求是訪問的同一個方法,是如何實現線程安全的?
SpringMVC Controller默認情況下是Singleton(單例)的,當request過來,不用每次創建Controller,會用原來的instance去處理。那麼當多個線程調用它的時候,會不會發生線程不安全呢?
1、先說明下 Controller默認情況 單例的問題:
使用Spring MVC有一段時間瞭,之前一直使用Struts2,在struts2中action都是原型(prototype)的, 說是因為線程安全問題,對於Spring MVC中bean默認都是(singleton)單例的,那麼用@Controller註解標簽註入的Controller類是單例實現的?
測試結果發現spring3中的controller默認是單例的,若是某個controller中有一個私有的變量i,所有請求到同一個controller時,使用的i變量是共用的,即若是某個請求中修改瞭這個變量a,則,在別的請求中能夠讀到這個修改的內容。 若是在@Controller之前增加@Scope(“prototype”),就可以改變單例模式為多例模式
以下是測試步驟,代碼與結果.
1. 如果是單例類型類的,那麼在Controller類中的類變量應該是共享的,如果不共享,就說明Controller類不是單例。
以下是測試代碼:
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class ExampleAction { private int singletonInt=1; @RequestMapping(value = "/test") @ResponseBody public String singleton(HttpServletRequest request, HttpServletResponse response) throws Exception { String data=request.getParameter("data"); if(data!=null&&data.length()>0){ try{ int paramInt= Integer.parseInt(data); singletonInt = singletonInt + paramInt; } catch(Exception ex){ singletonInt+=10; } }else{ singletonInt+=1000; } return String.valueOf(singletonInt); } }
分別三次請求: http://localhost:8080/example/test.do?data=15
得到的返回結果如下。
第一次: singletonInt=15
第二次: singletonInt=30
第三次: singletonInt=45
從以上結果可以得知,singletonInt的狀態是共享的,因此Controller是單例的。
2、對別Struts與springmvc對比
Struts2:默認prototype,Struts2 是基於類的,處於線程安全的考慮,采用瞭prototype模式,也就是說每次請求都會新建一個類來處理,自然就沒有線程安全問題瞭,每次請求的類和數據都是單獨的。
Springmvc:默認singleton 單例模式,Springmvc 是基於方法的,同一個url的請求是同一個實例處理的。每次請求都會把請求參數傳遞到同一個方法中,此時如果類裡面有成員變量,那麼這個變量就不是線程安全的瞭(例如上面的例子 private int singletonInt=1; 這個變量如果想線程安全則可以用ThreadLocal)。
在類中沒有成員變量的前提下則是線程安全的。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Java面試題沖刺第七天–Spring框架1
- spring單例如何改多例
- 解析Spring中@Controller@Service等線程安全問題
- Spring Bean的線程安全問題
- Spring超詳細講解註解開發