一区二区三区三上|欧美在线视频五区|国产午夜无码在线观看视频|亚洲国产裸体网站|无码成年人影视|亚洲AV亚洲AV|成人开心激情五月|欧美性爱内射视频|超碰人人干人人上|一区二区无码三区亚洲人区久久精品

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

多線程引發(fā)的慘案!

小林coding ? 來源:小林coding ? 2023-02-07 15:30 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

今天分享一位朋友線上出現(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)化為多線程的,如下

97f0512e-a44b-11ed-bfe3-dac502259ad0.pngimg

這樣的話,搜索請求的耗時(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)

  1. 無鎖化提升并發(fā)性能

  2. 簡化變量的傳遞邏輯

1.無鎖化提升并發(fā)性能

先說第一個(gè),無鎖化提升并發(fā)性能,影響并發(fā)的原因有很多,其中一個(gè)很重要的原因就是鎖,為了防止對共享變量的競用,不得不對共享變量加鎖

980c0f36-a44b-11ed-bfe3-dac502259ad0.png

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

981ee5ac-a44b-11ed-bfe3-dac502259ad0.png無鎖化

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()這樣的方法即可獲得線程本地變量

可能一些朋友會好奇線程本地變量是如何存儲的,一圖勝千言

982d93b8-a44b-11ed-bfe3-dac502259ad0.png

每一個(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>threadLocal
=newThreadLocal<>();

@Override
publicvoidrun(){
//clientInfo初始化
MapclientInfo=xxx;
threadLocal.set(clientInfo);
test1();
}

publicvoidtest1(){
test2();
}

publicvoidtest2(){
testX();
}
...

publicvoidtestX(){
MapclientInfo=threadLocal.get();
}
}

中間定義的任何方法都無需為了傳遞 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 方法主要步驟如下

  1. 首先需要獲取當(dāng)前線程

  2. 其次獲取當(dāng)前線程的 ThreadLocalMap

  3. 進(jìn)而再去獲取相應(yīng)的本地變量值

  4. 如果沒有的話則調(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 一定安全可靠嗎?


審核編輯 :李倩


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • 多線程
    +關(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)載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關(guān)推薦
    熱點(diǎn)推薦

    Java多線程的用法

    本文將介紹一下Java多線程的用法。 基礎(chǔ)介紹 什么是多線程 指的是在一個(gè)進(jìn)程中同時(shí)運(yùn)行多個(gè)線程,每個(gè)線程都可以獨(dú)立執(zhí)行不同的任務(wù)或操作。 與單線程
    的頭像 發(fā)表于 09-30 17:07 ?1214次閱讀

    多線程技術(shù)在串口通信中的應(yīng)用

            首先介紹了多線程技術(shù)的基本原理,然后討論了多線程技術(shù)在串口通信中的應(yīng)用,并給出了實(shí)現(xiàn)的方法和步驟。關(guān)鍵詞:多線程;串口通信;事件
    發(fā)表于 09-04 09:10 ?18次下載

    多線程與聊天室程序的創(chuàng)建

    多線程程序的編寫,多線程應(yīng)用中容易出現(xiàn)的問題?;コ鈱ο蟮闹v解,如何采用互斥對象來實(shí)現(xiàn)多線程的同步。如何利用命名互斥對象保證應(yīng)用程序只有一個(gè)實(shí)例運(yùn)行。應(yīng)用多線程編寫網(wǎng)絡(luò)聊天室程序。
    發(fā)表于 05-16 15:22 ?0次下載

    設(shè)計(jì)多線程和多核系統(tǒng)

    如果您的微控制器應(yīng)用程序需要處理數(shù)字音頻,請考慮采用多線程方法。使用多線程設(shè)計(jì)方法可以使設(shè)計(jì)者以簡單的方式重用其部分設(shè)計(jì)。
    發(fā)表于 08-14 15:42 ?9次下載
    設(shè)計(jì)<b class='flag-5'>多線程</b>和多核系統(tǒng)

    linux多線程編程技術(shù)

    1 引言 線程(thread)技術(shù)早在60年代就被提出,但真正應(yīng)用多線程到操作系統(tǒng)中去,是在80年代中期,solaris是這方面的佼佼者。傳統(tǒng)的 Unix也支持線程的概念,但是在一個(gè)進(jìn)程
    發(fā)表于 10-24 16:01 ?5次下載

    多線程好還是單線程好?單線程多線程的區(qū)別 優(yōu)缺點(diǎn)分析

    摘要:如今單線程多線程已經(jīng)得到普遍運(yùn)用,那么到底多線程好還是單線程好呢?單線程多線程的區(qū)別又
    發(fā)表于 12-08 09:33 ?8.2w次閱讀

    mfc多線程編程實(shí)例及代碼,mfc多線程間通信介紹

    摘要:本文主要以MFC多線程為中心,分別對MFC多線程的實(shí)例、MFC多線程之間的通信展開的一系列研究,下面我們來看看原文。
    發(fā)表于 12-08 15:23 ?1.8w次閱讀
    mfc<b class='flag-5'>多線程</b>編程實(shí)例及代碼,mfc<b class='flag-5'>多線程</b>間通信介紹

    什么是多線程編程?多線程編程基礎(chǔ)知識

    摘要:多線程編程是現(xiàn)代軟件技術(shù)中很重要的一個(gè)環(huán)節(jié)。要弄懂多線程,這就要牽涉到多進(jìn)程。本文主要以多線程編程以及多線程編程相關(guān)知識而做出的一些結(jié)論。
    發(fā)表于 12-08 16:30 ?1.3w次閱讀

    SpringBoot實(shí)現(xiàn)多線程

    SpringBoot實(shí)現(xiàn)多線程
    的頭像 發(fā)表于 01-12 16:59 ?2171次閱讀
    SpringBoot實(shí)現(xiàn)<b class='flag-5'>多線程</b>

    labview AMC多線程

    labview_AMC多線程
    發(fā)表于 08-21 10:31 ?34次下載

    多線程idm下載軟件

    多線程idm下載軟件
    發(fā)表于 10-23 09:23 ?0次下載

    多線程如何保證數(shù)據(jù)的同步

    多線程編程是一種并發(fā)編程的方法,意味著程序中同時(shí)運(yùn)行多個(gè)線程,每個(gè)線程可獨(dú)立執(zhí)行不同的任務(wù),共享同一份數(shù)據(jù)。由于多線程并發(fā)執(zhí)行的特點(diǎn),會引發(fā)
    的頭像 發(fā)表于 11-17 14:22 ?1690次閱讀

    mfc多線程編程實(shí)例

    (圖形用戶界面)應(yīng)用程序的開發(fā)。在這篇文章中,我們將重點(diǎn)介紹MFC中的多線程編程。 多線程編程在軟件開發(fā)中非常重要,它可以實(shí)現(xiàn)程序的并發(fā)執(zhí)行,提高程序的效率和響應(yīng)速度。MFC提供了豐富的多線程支持,可以輕松地實(shí)現(xiàn)
    的頭像 發(fā)表于 12-01 14:29 ?1929次閱讀

    java實(shí)現(xiàn)多線程的幾種方式

    Java實(shí)現(xiàn)多線程的幾種方式 多線程是指程序中包含了兩個(gè)或以上的線程,每個(gè)線程都可以并行執(zhí)行不同的任務(wù)或操作。Java中的多線程可以提高程序
    的頭像 發(fā)表于 03-14 16:55 ?1311次閱讀

    socket 多線程編程實(shí)現(xiàn)方法

    在現(xiàn)代網(wǎng)絡(luò)編程中,多線程技術(shù)被廣泛應(yīng)用于提高服務(wù)器的并發(fā)處理能力。Socket編程是網(wǎng)絡(luò)通信的基礎(chǔ),而將多線程技術(shù)應(yīng)用于Socket編程,可以顯著提升服務(wù)器的性能。 多線程編程的基本概念 多線
    的頭像 發(fā)表于 11-12 14:16 ?991次閱讀