今天分享一位朋友線上出現(xiàn)了一個(gè)比較嚴(yán)重的故障,這個(gè)故障是多線程使用不當(dāng)引起的。
挺有代表性的,所以分享給大家,希望能幫大家避坑。
問題簡述
先簡單介紹一下問題產(chǎn)生的背景,我們有個(gè)返利業(yè)務(wù)。
其中有個(gè)搜索場景,這個(gè)場景是用戶在 app 輸入搜索關(guān)鍵詞,然后 server 會根據(jù)這個(gè)關(guān)鍵詞到各個(gè)平臺(如淘寶,京東,拼多多等)調(diào)一下搜索接口,聚合這些搜索結(jié)果后再返回給用戶。
最開始這個(gè)搜索場景處理是單線程的,但隨著接入的平臺越來越多,搜索請求耗時(shí)也越來越長,由于每個(gè)平臺的搜索請求都是獨(dú)立的,很顯然,單線程是可以優(yōu)化為多線程的,如下

這樣的話,搜索請求的耗時(shí)就只取決于搜索接口耗時(shí)最長的那個(gè)平臺。
所以使用多線程顯然對接口性能是一個(gè)極大的優(yōu)化,但使用多線程改造上線后,短時(shí)間內(nèi)社群中有多名用戶反饋前臺展示「APP 需要升級的提示」。
經(jīng)定位后發(fā)現(xiàn)是因?yàn)樵诙嗑€程中無法獲取客戶端信息,由于客戶端信息缺失,導(dǎo)致返回給用戶需要升級的提示,偽代碼如下:
//開啟多線程處理
newThread(newRunnable(){
@Override
publicvoidrun(){
MapclientInfoMap=Context.getContext().getClientInfo();
//無法獲取客戶端信息,返回需要升級的信息
if(clientInfoMap==null){
thrownewException("版本號過低,請升級版本");
}
Stringversion=clientInfoMap.get("version");
//以下正常邏輯
....
}
}).start();
畫外音:在生產(chǎn)中多線程使用的是線程池來實(shí)現(xiàn),這里為了方便演示,直接 new Thread,效果都一樣,大家知道即可。
那么問題來了,改成多線程后客戶端信息怎么就取不到了呢?
要搞清楚這個(gè)問題,就得先了解客戶端信息是如何存儲的了。
Threadlocal 簡介
不同客戶端請求的客戶端信息(wifi 還是 4G,機(jī)型,app名稱,電量等)顯然不一樣,dubbo 業(yè)務(wù)線程拿到客戶端請求后首先會將有用的請求信息提取出來(如本文中的 MapclientInfo)。
但這個(gè) clientInfo 可能會在線程調(diào)用的各個(gè)方法中用到,于是如何存儲就成為了一個(gè)現(xiàn)實(shí)的問題。
相信有經(jīng)驗(yàn)的朋友一下就想到了,沒錯(cuò),用 Threadlocal !
為什么用它,它有什么優(yōu)勢,簡單來說有兩點(diǎn)
-
無鎖化提升并發(fā)性能
-
簡化變量的傳遞邏輯
1.無鎖化提升并發(fā)性能
先說第一個(gè),無鎖化提升并發(fā)性能,影響并發(fā)的原因有很多,其中一個(gè)很重要的原因就是鎖,為了防止對共享變量的競用,不得不對共享變量加鎖

如果對共享變量爭用的線程數(shù)增多,顯然會嚴(yán)重影響系統(tǒng)的并發(fā)度,最好的辦法就是使用“影分身術(shù)”為每個(gè)線程都創(chuàng)建一個(gè)線程本地變量,這樣就避免了對共享變量的競用,也就實(shí)現(xiàn)了無鎖化

ThreadLocal 即線程本地變量,它可以為每個(gè)線程創(chuàng)建一份線程本地變量,使用方法如下
staticThreadLocalthreadLocal1=newThreadLocal(){
@Override
protectedSimpleDateFormatinitialValue(){
returnnewSimpleDateFormat("yyyy-MM-dd");
}
};
publicStringformatDate(Datedate){
returnthreadLocal1.get().format(date);
}
這樣的話每個(gè)線程就獨(dú)享一份與其他線程無關(guān)的 SimpleDateFormat 實(shí)例副本,它們調(diào)用 formatDate 時(shí)使用的 SimpleDateFormat 實(shí)例也是自己獨(dú)有的副本,無論對副本怎么操作對其他線程都互不影響
通過以上例子我們可以看出,可以通過 new ThreadLocal
+ initialValue
來為創(chuàng)建的 ThreadLocal 實(shí)例初始化本地變量(initialValue
方法會在首次調(diào)用 get 時(shí)被調(diào)用以初始化本地變量)。
當(dāng)然,如果之后需要修改本地變量的話,也可以用以下方式來修改
threadLocal1.set(newSimpleDateFormat("yyyy-MM-dd"))
而使用 threadLocal1.get()
這樣的方法即可獲得線程本地變量
可能一些朋友會好奇線程本地變量是如何存儲的,一圖勝千言

每一個(gè)線程(Thread)內(nèi)部都有一個(gè) ThreadLocalMap, ThreadLocal 的 get 和 set 操作其實(shí)在底層都是針對 ThreadLocalMap 進(jìn)行操作的。
publicclassThreadimplementsRunnable{
/*ThreadLocalvaluespertainingtothisthread.Thismapismaintained
*bytheThreadLocalclass.*/
ThreadLocal.ThreadLocalMapthreadLocals=null;
}
它與 HashMap 類似,存儲的都是鍵值對,只不過每一項(xiàng)(Entry)中的 key 為 threadlocal 變量(如上文案例中的 threadLocal1),value 才為我們要存儲的值(如上文中的 SimpleDateFormat 實(shí)例)。
此外它們在碰到 hash 沖突時(shí)的處理策略也不同,HashMap 在碰到 hash 沖突時(shí)采用的是鏈表法,而 ThreadLocalMap 采用的是線性探測法
2.簡化變量的傳遞邏輯
接下來我們來看使用 ThreadLocal 的等二個(gè)好處,簡化變量的傳遞邏輯。
線程在處理業(yè)務(wù)邏輯時(shí)可能會調(diào)用幾十個(gè)方法,如果這些方法中只有幾個(gè)需要用到 clientInfo,難道要在這幾十個(gè)方法中定義一個(gè) clientInfo 參數(shù)來層層傳遞嗎,顯然不現(xiàn)實(shí)。
那該怎么辦呢,使用 ThreadLocal 即可解決此問題。
由上文可知通過 ThreadLocal 設(shè)置的本地變量是同 threadlocal 一起保存在 Thread 的 ThreadLocalMap 這個(gè)內(nèi)部類中的,所以可在線程調(diào)用的任意方法中取出,偽代碼如下:
publicclassThreadLocalWithUserContextimplementsRunnable{
privatestaticThreadLocal
中間定義的任何方法都無需為了傳遞 clientInfo 而定義一個(gè)額外的變量,代碼優(yōu)雅了不少。
由以上分析可知,使用 ThreadLocal 確實(shí)比較方便。
在此我們先停下來思考一個(gè)問題:如果線程在調(diào)用過程中只用到一個(gè) clientInfo 這樣的信息,只定義一個(gè) ThreadLocal 變量當(dāng)然就夠了,但實(shí)際上在使用過程中我們可能要傳遞多個(gè)類似 clientInfo 這樣的信息(如 userId,cookie,header),難道因此要定義多個(gè) ThreadLocal 變量嗎?
這么做不是不可以,但不夠優(yōu)雅。
更合適的做法是我們只定義一個(gè) ThreadLocal 變量,變量存的是一個(gè)上下文對象,其他像 clientInfo,userId,header 等信息就作為此上下文對象的屬性即可,代碼如下:
publicfinalclassContext{
privatestaticfinalThreadLocalLOCAL=newThreadLocal(){
protectedContextinitialValue(){
returnnewContext();
}
};
privateLonguid;//用戶uid
privateMapclientInfo;//客戶端信息
privateMapheaders=null;//請求頭信息
privateMap>cookies=null;//請求cookie
publicstaticContextgetContext(){
return(Context)LOCAL.get();
}
}
這樣的話我們可通過 Context.getContext().getXXX()
的形式來獲取線程所需的信息,通過這樣的方式我們不僅避免了定義無數(shù) ThreadLocal 變量的煩惱,而且還收攏了上下文信息的管理。
通過以上介紹相信大家也都知道了 clientInfo 其實(shí)是借由 ThreadLocal 存儲的。
認(rèn)清了這個(gè)事實(shí)后那我們現(xiàn)在再回頭看開頭的生產(chǎn)問題:將單線程改成多線程后,為什么在新線程中就拿不到 clientInfo 了?
問題剖析
源碼之下無秘密,我們查看一下源碼來一探究竟,獲取本地變量的值使用的是 ThreadLocal.get 方法,那就來看下這個(gè)方法:
publicclassThreadLocal<T>{
publicTget(){
//1.先獲取當(dāng)前線程
Threadt=Thread.currentThread();
//2.再獲取當(dāng)前線程的ThreadLocalMap
ThreadLocalMapmap=getMap(t);
if(map!=null){
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
Tresult=(T)e.value;
returnresult;
}
}
returnsetInitialValue();
}
}
可以看到 get 方法主要步驟如下
-
首先需要獲取當(dāng)前線程
-
其次獲取當(dāng)前線程的 ThreadLocalMap
-
進(jìn)而再去獲取相應(yīng)的本地變量值
-
如果沒有的話則調(diào)用 initiaValue 方法來初始化本地變量
由此可知當(dāng)我們調(diào)用 threadlocal.get 時(shí),會拿到當(dāng)前線程的 ThreadLocalMap,然后再去拿 entry 中的本地變量,而對多線程來說,新線程的 ThreadLocalMap 里面的東西本來就未做任何設(shè)置,是空的,拿不到線程本地變量也就合情合理了
解決方案
問題清楚了,那怎么解決呢,不難得知主要有兩種方案
1.我們之前是在新線程的執(zhí)行方法中調(diào)用 threadlocal.get 方法,可以改成先從當(dāng)前執(zhí)行線程中調(diào)用 threadlocal.get 獲得 clientInfo,然后再把 clientInfo 傳入新線程,偽代碼如下:
//先從當(dāng)前線程的Context中獲取clientInfo
MapclientInfoMap=Context.getContext().getClientInfo();
newThread(newRunnable(){
@Override
publicvoidrun(){
//此時(shí)的clientInfoMap由于是在新線程創(chuàng)建前獲取的,肯定是有值的
Stringversion=clientInfoMap.get("version");
//以下正常邏輯
....
}
}).start();
2.只需把 ThreadLocal 換成 InheritableThreadLocal,如下:
publicfinalclassContext{
privatestaticfinalInheritableThreadLocalLOCAL=newInheritableThreadLocal(){
protectedContextinitialValue(){
returnnewContext();
}
};
publicstaticContextgetContext(){
return(Context)LOCAL.get();
}
}
newThread(newRunnable(){
@Override
publicvoidrun(){
//此時(shí)的clientInfo能正常獲取到
MapclientInfo=Context.getContext().getClientInfo();
Stringversion=clientInfo.get("version");
//以下正常邏輯
....
}
}).start();
為什么 InheritableThreadLocal 能有這么神奇,背后的原理是什么?
由前文介紹我們得知,ThreadLocal 變量最終是存在 ThreadLocalMap 中的。
那么能否在創(chuàng)建新線程的時(shí)候,把當(dāng)前線程的 ThreadLocalMap 復(fù)制給新線程的 ThreadLocalMap 呢?
這樣的話即便你從新線程中調(diào)用 threadlocal.get 也照樣能獲得對應(yīng)的本地變量,和 InheritableThreadLocal 相關(guān)的底層干的就是這個(gè)事。
我們先來瞧一瞧 InheritableThreadLocal 長啥樣:
publicclassInheritableThreadLocal<T>extendsThreadLocal<T>{
ThreadLocalMapgetMap(Threadt){
returnt.inheritableThreadLocals;
}
voidcreateMap(Threadt,TfirstValue){
t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);
}
}
由此可知 InheritableThreadLocal 其實(shí)是繼承自 ThreadLocal 類的。
此外我們在 getMap 和 createMap 這兩個(gè)方法中也發(fā)現(xiàn)它的底層其實(shí)是用 inheritableThreadLocals 來存儲的,而 ThreadLocal 用的是 threadLocals 變量存儲的。
publicclassThreadimplementsRunnable{
//ThreadLocal實(shí)例的底層存儲
ThreadLocal.ThreadLocalMapthreadLocals=null;
//inheritableThreadLocals實(shí)例的底層存儲
ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;
}
知道了這些,我們再來看下創(chuàng)建線程時(shí)涉及到的 inheritableThreadLocals 復(fù)制相關(guān)的關(guān)鍵代碼如下:
public
classThreadimplementsRunnable{
publicThread(){
init(null,null,"Thread-"+nextThreadNum(),0);
}
privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,
longstackSize){
init(g,target,name,stackSize,null,true);
}
privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,
longstackSize,AccessControlContextacc,
booleaninheritThreadLocals){
...
Threadparent=currentThread();
if(inheritThreadLocals&&parent.inheritableThreadLocals!=null)
//將當(dāng)前線程的inheritableThreadLocals復(fù)制給新創(chuàng)建線程的inheritableThreadLocals
this.inheritableThreadLocals=
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
}
由此可知,在創(chuàng)建新線程時(shí),在初始化時(shí)其實(shí)相關(guān)邏輯是幫我們干了復(fù)制 inheritableThreadLocals 的操作,至此真相大白!
總結(jié)
看完本文,相信大家對 Threadlocal 與 InheritableThreadLocal 的使用及其底層原理的掌握已不存在疑問。
這也提醒我們熟練地掌握一個(gè)組件或一項(xiàng)技術(shù)最好的方式還是熟讀它的源碼,畢竟源碼之下無秘密。
當(dāng)我們使用到別人封裝好的組件或類時(shí),如果有興趣也可以也看一下它的源碼。
以本文為例,其實(shí)我們工程中多處地方都使用了 Context.getContext().getClientInfo();
這樣的獲取客戶端信息的形式,用慣了導(dǎo)致在多線程環(huán)境下沒有引起警惕,以致踩了坑。
另外需要注意的是 ThreadLocal 使用不當(dāng)可能導(dǎo)致內(nèi)存泄漏,需要在線程結(jié)束后及時(shí) remove 掉,這些技術(shù)細(xì)節(jié)不是本文重點(diǎn),故而沒有深入詳解,有興趣的大家可以去查閱相關(guān)資料。
歷史好文:
多個(gè)線程為了同個(gè)資源打起架來了,該如何讓他們安分?
美團(tuán)三面:一直追問我, MySQL 幻讀被徹底解決了嗎?
原來墻,是這么把我 TCP 連接干掉的!
面試官:你確定 Redis 是單線程的進(jìn)程嗎?
字節(jié)一面:HTTPS 一定安全可靠嗎?
審核編輯 :李倩
-
多線程
+關(guān)注
關(guān)注
0文章
279瀏覽量
20408 -
代碼
+關(guān)注
關(guān)注
30文章
4898瀏覽量
70586 -
變量
+關(guān)注
關(guān)注
0文章
614瀏覽量
28930
原文標(biāo)題:多線程引發(fā)的慘案!直接把年終給干沒了
文章出處:【微信號:小林coding,微信公眾號:小林coding】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
Java多線程的用法
多線程技術(shù)在串口通信中的應(yīng)用
多線程與聊天室程序的創(chuàng)建
設(shè)計(jì)多線程和多核系統(tǒng)

linux多線程編程技術(shù)
多線程好還是單線程好?單線程和多線程的區(qū)別 優(yōu)缺點(diǎn)分析
mfc多線程編程實(shí)例及代碼,mfc多線程間通信介紹

評論