前言
記錄一下在測試過程中,遇到的一個(gè)有關(guān)ThreadLocal的問題,順便學(xué)習(xí)一下ThreadLocal相關(guān)的知識。
ThreadLocal介紹
ThreadLocal是一個(gè)關(guān)于創(chuàng)建線程局部變量的類。
要點(diǎn):
- 在當(dāng)前線程中,任何一個(gè)地方都可以訪問到ThreadLocal的值。
- 每個(gè)線程里面都有一個(gè)ThreadLocalMap變量,初始值為null,這個(gè)變量的值由ThreadLocal來維護(hù)
- 當(dāng)前線程保存在ThreadLocal中的值只能被當(dāng)前線程訪問,一般情況下其他線程訪問不到。
- ThreadLocalMap存儲數(shù)據(jù)方式類似Map的key-value存儲方式,只不過ThreadLocal是以當(dāng)前線程為key,value可以為任意類型的值
問題場景
最近項(xiàng)目需要上線一個(gè)大版本,此次版本對前端APP新、老版本發(fā)起的請求做了不同的加密處理,經(jīng)過討論,需要在后臺做版本兼容。
兼容的流程:
- APP端在請求頭里面新增一個(gè)字段作為新版本APP的標(biāo)識,如:varA:123
- 后端在SpringDispatcherServlet中判斷varA是否為空,若不為空則把它放入ThreadLocal變量中
if (StringUtil.isNotEmpty(varA)){ ThreadContext.put(ThreadContext.FLAG, varA); } - 然后在JsonHttpMessageConverter(自定義請求解析類)中,根據(jù)varA是否為空來決定采取哪種解密方式來解密請求
String flag = ThreadLocal.get(ThreadContext.FLAG); if (StringUtil.isNotEmpty(flag)){ //新版本解密方式 }else{ //老版本解密方式 } - 邏輯處理
- 響應(yīng)請求
問題描述
按照上面的兼容流程做完代碼更改之后,在本地測試沒有問題,但是放在測試環(huán)境,由測試人員測試就有問題。
具體問題描述:
- 老版本APP發(fā)起的請求在后臺解密時(shí)會進(jìn)入新版本APP解密方式的判斷里面去,但是只是部分請求才會出現(xiàn)此情況
分析結(jié)果
我們知道,后端應(yīng)用服務(wù)器在處理請求時(shí),會對每一個(gè)請求分配一個(gè)線程來處理,如果每次來一個(gè)請求都去新開一個(gè)線程,然后響應(yīng)請求之后又去銷毀線程,這樣的結(jié)果不僅會增加請求響應(yīng)時(shí)間,而且還會大大提高系統(tǒng)資源消耗。
所以為了適應(yīng)高并發(fā)請求,在應(yīng)用服務(wù)器端都會使用線程池來處理請求,效果是減少系統(tǒng)資源開銷以及加快請求響應(yīng)時(shí)間。
前面講到,由于ThreadLocal是以當(dāng)前線程為key,所以如果前后有兩條請求發(fā)到后臺,并且這兩條請求都是使用的線程池里面的同一個(gè)線程。并且第一條是新版本APP發(fā)過來的帶有標(biāo)識的請求,第二條是老版本APP發(fā)過來的不帶標(biāo)識的請求。
第一條請求把標(biāo)識存入ThreadLocal變量中,在響應(yīng)完請求之后沒有及時(shí)的清理掉ThreadLocal中的值
當(dāng)?shù)诙l不帶標(biāo)識的請求到來時(shí),由于在SpringDispatcherServlet中做了不為空才把標(biāo)識放入ThreadLocal中,所以這里就沒有更新ThreadLocal中的值,但其實(shí)由于前面一個(gè)請求響應(yīng)之后沒有清理掉ThreadLocal中的值,所以在JsonHttpMessageConverter中獲取當(dāng)前線程的標(biāo)識時(shí),還是有值,這樣就會進(jìn)入新版本的解密方式中去。
問題處理
兩種方式:
在SpringDispatcherServlet中不做判空處理,從請求中不管獲取到什么值都存入ThreadLocal變量中,以此達(dá)到實(shí)時(shí)更新值的效果
-
在響應(yīng)完請求之后移除ThreadLocal中想要移除的值或清空ThreadLocal里面當(dāng)前線程保存的所有值
ThreadContext.remove(ThreadContext.FLAG); 或者清空所有 ThreadContext.remove();
最后我采取的第二種方式,因?yàn)榘催壿嬍?strong>ThreadLocal里面的數(shù)據(jù)只適合在本次請求中使用,使用完了之后就得清理掉