使用springboot單例模式與線程安全問(wèn)題踩的坑
springboot單例模式與線程安全問(wèn)題踩的坑
最近有客戶(hù)反映,使用公司產(chǎn)品時(shí),偶爾會(huì)存在崩潰情況,自己測(cè)試無(wú)問(wèn)題,然后去查日志,是報(bào)空指針。
于是順藤摸瓜 往上找,好嘛,之前的開(kāi)發(fā)使用了成員變量,感覺(jué)問(wèn)題就是在這里了,因?yàn)楸娝苤?,springboot 采用的是單例模式,所以,使用成員變量時(shí)一定要謹(jǐn)慎。
下面上一張?jiān)擃?lèi)的截圖:

大家可能看到了,該類(lèi)上面加上了@Scope("prototype") 注解,該注解的作用是將該類(lèi)變成多例模式。講道理因?yàn)樽優(yōu)榱硕嗬?,?yīng)該不會(huì)有線程問(wèn)題了。
我先說(shuō)下我這邊的一個(gè)代碼環(huán)境,上面大家看到的BaseController這個(gè)類(lèi)里面有個(gè)init方法,會(huì)在繼承它的類(lèi)的所有方法前執(zhí)行。

使用的是@ModelAttribute注解,這個(gè)注解的意思是,在該controller的所有方法前執(zhí)行,意在初始化,我猜測(cè)之前的同事應(yīng)該是為了獲取相同的一些參數(shù),抽調(diào)出來(lái)做一個(gè)父類(lèi),隨著迭代,別的同事為了方便,拿來(lái)就用,導(dǎo)致很多controller繼承了該類(lèi)。
@Scope("prototype")注解:
大家設(shè)想一下,若父類(lèi)加了@Scope("prototype")注解,子類(lèi)controller并沒(méi)有加該注解,會(huì)怎樣呢?該注解是否還有意義?再比如,我在某service上加上@Scope("prototype")注解,但調(diào)用的controller沒(méi)有加@Scope("prototype")注解,那么會(huì)出現(xiàn)什么樣的結(jié)果呢?大家可以去測(cè)試一下,測(cè)試方法也很簡(jiǎn)單,就是在對(duì)應(yīng)的父類(lèi)或service的無(wú)參構(gòu)造方法里打印該類(lèi)的地址。
下面說(shuō)下我的測(cè)試結(jié)果:
先說(shuō)父類(lèi)上加了@Scope("prototype")注解,子類(lèi)上沒(méi)有加這種情況。結(jié)果是,同一子類(lèi)繼承的為同一父類(lèi),不同子類(lèi)繼承為不同父類(lèi)。理解一下,很簡(jiǎn)單,因?yàn)閟pringboot為單例模式,所以子類(lèi)為單例,那么只有一個(gè)子類(lèi),父類(lèi)肯定是一樣的。所以,不同線程過(guò)來(lái)使用的為同一變量,就會(huì)有問(wèn)題。
同理:
在service上標(biāo)注@Scope("prototype")注解,那在同一個(gè)controller里,該service還是同一個(gè),也就是說(shuō)還是單例的,在不同的controller里 是不同的。測(cè)試方法同上。
現(xiàn)在說(shuō)下解決方法:
1、是在繼承該controller的子類(lèi)上都加上@Scope("prototype")注解。這樣做的好處是簡(jiǎn)單。壞處也同樣明顯,因?yàn)槭嵌嗬?,那么就?huì)產(chǎn)生大量的實(shí)體類(lèi),占用大量?jī)?nèi)存,若是回收不及時(shí),有可能會(huì)出現(xiàn)內(nèi)存溢出。
2、是將變量私有化,比如使用線程變量,對(duì)變量加鎖等,技術(shù)上會(huì)復(fù)雜一些,而且調(diào)試不太好調(diào)試。說(shuō)不定那些地方就會(huì)出現(xiàn)問(wèn)題,畢竟是老代碼。
3、將該類(lèi)轉(zhuǎn)換為攔截器,將變量放入request里,用的時(shí)候取出來(lái)。
SpringMVC 或 SpringBoot 默認(rèn)是單例模式(Singleton)
多個(gè)請(qǐng)求是訪問(wèn)的同一個(gè)方法,是如何實(shí)現(xiàn)線程安全的?
SpringMVC Controller默認(rèn)情況下是Singleton(單例)的,當(dāng)request過(guò)來(lái),不用每次創(chuàng)建Controller,會(huì)用原來(lái)的instance去處理。那么當(dāng)多個(gè)線程調(diào)用它的時(shí)候,會(huì)不會(huì)發(fā)生線程不安全呢?
1、先說(shuō)明下 Controller默認(rèn)情況 單例的問(wèn)題:
使用Spring MVC有一段時(shí)間了,之前一直使用Struts2,在struts2中action都是原型(prototype)的, 說(shuō)是因?yàn)榫€程安全問(wèn)題,對(duì)于Spring MVC中bean默認(rèn)都是(singleton)單例的,那么用@Controller注解標(biāo)簽注入的Controller類(lèi)是單例實(shí)現(xiàn)的?
測(cè)試結(jié)果發(fā)現(xiàn)spring3中的controller默認(rèn)是單例的,若是某個(gè)controller中有一個(gè)私有的變量i,所有請(qǐng)求到同一個(gè)controller時(shí),使用的i變量是共用的,即若是某個(gè)請(qǐng)求中修改了這個(gè)變量a,則,在別的請(qǐng)求中能夠讀到這個(gè)修改的內(nèi)容。 若是在@Controller之前增加@Scope(“prototype”),就可以改變單例模式為多例模式
以下是測(cè)試步驟,代碼與結(jié)果.
1. 如果是單例類(lèi)型類(lèi)的,那么在Controller類(lèi)中的類(lèi)變量應(yīng)該是共享的,如果不共享,就說(shuō)明Controller類(lèi)不是單例。
以下是測(cè)試代碼:
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);
}
}
分別三次請(qǐng)求: http://localhost:8080/example/test.do?data=15
得到的返回結(jié)果如下。
第一次: singletonInt=15
第二次: singletonInt=30
第三次: singletonInt=45
從以上結(jié)果可以得知,singletonInt的狀態(tài)是共享的,因此Controller是單例的。
2、對(duì)別Struts與springmvc對(duì)比
Struts2:默認(rèn)prototype,Struts2 是基于類(lèi)的,處于線程安全的考慮,采用了prototype模式,也就是說(shuō)每次請(qǐng)求都會(huì)新建一個(gè)類(lèi)來(lái)處理,自然就沒(méi)有線程安全問(wèn)題了,每次請(qǐng)求的類(lèi)和數(shù)據(jù)都是單獨(dú)的。
Springmvc:默認(rèn)singleton 單例模式,Springmvc 是基于方法的,同一個(gè)url的請(qǐng)求是同一個(gè)實(shí)例處理的。每次請(qǐng)求都會(huì)把請(qǐng)求參數(shù)傳遞到同一個(gè)方法中,此時(shí)如果類(lèi)里面有成員變量,那么這個(gè)變量就不是線程安全的了(例如上面的例子 private int singletonInt=1; 這個(gè)變量如果想線程安全則可以用ThreadLocal)。
在類(lèi)中沒(méi)有成員變量的前提下則是線程安全的。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Eclipse+Java+Swing+Mysql實(shí)現(xiàn)工資管理系統(tǒng)
這篇文章主要介紹了Eclipse+Java+Swing+Mysql實(shí)現(xiàn)工資管理系統(tǒng),對(duì)正在工作或者學(xué)習(xí)的你有一定的參考價(jià)值,需要的朋友可以參考一下2022-01-01
SpringCloud Nacos作為配置中心超詳細(xì)講解
這篇文章主要介紹了Springcloud中的Nacos作為配置中心,本文以用戶(hù)微服務(wù)為例,進(jìn)行統(tǒng)一的配置,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12
SpringBoot入坑筆記之spring-boot-starter-web 配置文件的使用
本篇向小伙伴介紹springboot配置文件的配置,已經(jīng)全局配置參數(shù)如何使用的。需要的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-01-01
SpringBoot?調(diào)用外部接口的三種實(shí)現(xiàn)方法
Spring Boot調(diào)用外部接口的方式有多種,常見(jiàn)的有以下三種方式:RestTemplate、Feign 和 WebClient,本文就詳細(xì)介紹一下,感興趣的可以了解一下2023-08-08
java底層JDK?Logging日志模塊處理細(xì)節(jié)深入分析
這篇文章主要為大家介紹了java底層JDK?Logging日志模塊處理細(xì)節(jié)深入分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
JAVA8 List<List<Integer>> list中再裝一個(gè)list轉(zhuǎn)成一個(gè)list操
這篇文章主要介紹了JAVA8 List<List<Integer>> list中再裝一個(gè)list轉(zhuǎn)成一個(gè)list操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08
JavaFX程序初次運(yùn)行創(chuàng)建數(shù)據(jù)庫(kù)并執(zhí)行建表SQL詳解
這篇文章主要介紹了JavaFX程序初次運(yùn)行創(chuàng)建數(shù)據(jù)庫(kù)并執(zhí)行建表SQL詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08
Mybatis-Plus進(jìn)階分頁(yè)與樂(lè)觀鎖插件及通用枚舉和多數(shù)據(jù)源詳解
這篇文章主要介紹了Mybatis-Plus的分頁(yè)插件與樂(lè)觀鎖插件還有通用枚舉和多數(shù)據(jù)源的相關(guān)介紹,文中代碼附有詳細(xì)的注釋?zhuān)信d趣的朋友來(lái)看看吧2022-03-03

