色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

多線程引發的慘案!

小林coding ? 來源:小林coding ? 2023-02-07 15:30 ? 次閱讀

今天分享一位朋友線上出現了一個比較嚴重的故障,這個故障是多線程使用不當引起的。

挺有代表性的,所以分享給大家,希望能幫大家避坑。

問題簡述

先簡單介紹一下問題產生的背景,我們有個返利業務。

其中有個搜索場景,這個場景是用戶在 app 輸入搜索關鍵詞,然后 server 會根據這個關鍵詞到各個平臺(如淘寶,京東,拼多多等)調一下搜索接口聚合這些搜索結果后再返回給用戶。

最開始這個搜索場景處理是單線程的,但隨著接入的平臺越來越多,搜索請求耗時也越來越長,由于每個平臺的搜索請求都是獨立的,很顯然,單線程是可以優化為多線程的,如下

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

這樣的話,搜索請求的耗時就只取決于搜索接口耗時最長的那個平臺。

所以使用多線程顯然對接口性能是一個極大的優化,但使用多線程改造上線后,短時間內社群中有多名用戶反饋前臺展示「APP 需要升級的提示」。

經定位后發現是因為在多線程中無法獲取客戶端信息,由于客戶端信息缺失,導致返回給用戶需要升級的提示,偽代碼如下:

//開啟多線程處理
newThread(newRunnable(){
@Override
publicvoidrun(){
MapclientInfoMap=Context.getContext().getClientInfo();
//無法獲取客戶端信息,返回需要升級的信息
if(clientInfoMap==null){
thrownewException("版本號過低,請升級版本");
}
Stringversion=clientInfoMap.get("version");


//以下正常邏輯
....
}
}).start();

畫外音:在生產中多線程使用的是線程池來實現,這里為了方便演示,直接 new Thread,效果都一樣,大家知道即可。

那么問題來了,改成多線程后客戶端信息怎么就取不到了呢?

要搞清楚這個問題,就得先了解客戶端信息是如何存儲的了。

Threadlocal 簡介

不同客戶端請求的客戶端信息(wifi 還是 4G,機型,app名稱,電量等)顯然不一樣,dubbo 業務線程拿到客戶端請求后首先會將有用的請求信息提取出來(如本文中的 MapclientInfo)。

但這個 clientInfo 可能會在線程調用的各個方法中用到,于是如何存儲就成為了一個現實的問題。

相信有經驗的朋友一下就想到了,沒錯,用 Threadlocal

為什么用它,它有什么優勢,簡單來說有兩點

  1. 無鎖化提升并發性能

  2. 簡化變量的傳遞邏輯

1.無鎖化提升并發性能

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

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

如果對共享變量爭用的線程數增多,顯然會嚴重影響系統的并發度,最好的辦法就是使用“影分身術”為每個線程都創建一個線程本地變量,這樣就避免了對共享變量的競用,也就實現了無鎖化

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

ThreadLocal 即線程本地變量,它可以為每個線程創建一份線程本地變量,使用方法如下

staticThreadLocalthreadLocal1=newThreadLocal(){
@Override
protectedSimpleDateFormatinitialValue(){
returnnewSimpleDateFormat("yyyy-MM-dd");
}
};

publicStringformatDate(Datedate){
returnthreadLocal1.get().format(date);
}

這樣的話每個線程就獨享一份與其他線程無關的 SimpleDateFormat 實例副本,它們調用 formatDate 時使用的 SimpleDateFormat 實例也是自己獨有的副本,無論對副本怎么操作對其他線程都互不影響

通過以上例子我們可以看出,可以通過 new ThreadLocal+ initialValue 來為創建的 ThreadLocal 實例初始化本地變量(initialValue 方法會在首次調用 get 時被調用以初始化本地變量)。

當然,如果之后需要修改本地變量的話,也可以用以下方式來修改

threadLocal1.set(newSimpleDateFormat("yyyy-MM-dd"))

而使用 threadLocal1.get()這樣的方法即可獲得線程本地變量

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

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

每一個線程(Thread)內部都有一個 ThreadLocalMap, ThreadLocal 的 get 和 set 操作其實在底層都是針對 ThreadLocalMap 進行操作的。

publicclassThreadimplementsRunnable{
/*ThreadLocalvaluespertainingtothisthread.Thismapismaintained
*bytheThreadLocalclass.*/
ThreadLocal.ThreadLocalMapthreadLocals=null;
}

它與 HashMap 類似,存儲的都是鍵值對,只不過每一項(Entry)中的 key 為 threadlocal 變量(如上文案例中的 threadLocal1),value 才為我們要存儲的值(如上文中的 SimpleDateFormat 實例)。

此外它們在碰到 hash 沖突時的處理策略也不同,HashMap 在碰到 hash 沖突時采用的是鏈表法,而 ThreadLocalMap 采用的是線性探測法

2.簡化變量的傳遞邏輯

接下來我們來看使用 ThreadLocal 的等二個好處,簡化變量的傳遞邏輯。

線程在處理業務邏輯時可能會調用幾十個方法,如果這些方法中只有幾個需要用到 clientInfo,難道要在這幾十個方法中定義一個 clientInfo 參數來層層傳遞嗎,顯然不現實。

那該怎么辦呢,使用 ThreadLocal 即可解決此問題。

由上文可知通過 ThreadLocal 設置的本地變量是同 threadlocal 一起保存在 Thread 的 ThreadLocalMap 這個內部類中的,所以可在線程調用的任意方法中取出,偽代碼如下:

publicclassThreadLocalWithUserContextimplementsRunnable{

privatestaticThreadLocal>threadLocal
=newThreadLocal<>();

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

publicvoidtest1(){
test2();
}

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

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

中間定義的任何方法都無需為了傳遞 clientInfo 而定義一個額外的變量,代碼優雅了不少。

由以上分析可知,使用 ThreadLocal 確實比較方便。

在此我們先停下來思考一個問題:如果線程在調用過程中只用到一個 clientInfo 這樣的信息,只定義一個 ThreadLocal 變量當然就夠了,但實際上在使用過程中我們可能要傳遞多個類似 clientInfo 這樣的信息(如 userId,cookie,header),難道因此要定義多個 ThreadLocal 變量嗎?

這么做不是不可以,但不夠優雅。

更合適的做法是我們只定義一個 ThreadLocal 變量,變量存的是一個上下文對象,其他像 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() 的形式來獲取線程所需的信息,通過這樣的方式我們不僅避免了定義無數 ThreadLocal 變量的煩惱,而且還收攏了上下文信息的管理。

通過以上介紹相信大家也都知道了 clientInfo 其實是借由 ThreadLocal 存儲的。

認清了這個事實后那我們現在再回頭看開頭的生產問題:將單線程改成多線程后,為什么在新線程中就拿不到 clientInfo 了?

問題剖析

源碼之下無秘密,我們查看一下源碼來一探究竟,獲取本地變量的值使用的是 ThreadLocal.get 方法,那就來看下這個方法:

publicclassThreadLocal<T>{
publicTget(){
//1.先獲取當前線程
Threadt=Thread.currentThread();
//2.再獲取當前線程的ThreadLocalMap
ThreadLocalMapmap=getMap(t);
if(map!=null){
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
Tresult=(T)e.value;
returnresult;
}
}
returnsetInitialValue();
}
}

可以看到 get 方法主要步驟如下

  1. 首先需要獲取當前線程

  2. 其次獲取當前線程的 ThreadLocalMap

  3. 進而再去獲取相應的本地變量值

  4. 如果沒有的話則調用 initiaValue 方法來初始化本地變量

由此可知當我們調用 threadlocal.get 時,會拿到當前線程的 ThreadLocalMap,然后再去拿 entry 中的本地變量,而對多線程來說,新線程的 ThreadLocalMap 里面的東西本來就未做任何設置,是空的,拿不到線程本地變量也就合情合理了

解決方案

問題清楚了,那怎么解決呢,不難得知主要有兩種方案

1.我們之前是在新線程的執行方法中調用 threadlocal.get 方法,可以改成先從當前執行線程中調用 threadlocal.get 獲得 clientInfo,然后再把 clientInfo 傳入新線程,偽代碼如下:

//先從當前線程的Context中獲取clientInfo
MapclientInfoMap=Context.getContext().getClientInfo();
newThread(newRunnable(){
@Override
publicvoidrun(){
//此時的clientInfoMap由于是在新線程創建前獲取的,肯定是有值的
Stringversion=clientInfoMap.get("version");


//以下正常邏輯
....
}
}).start();

2.只需把 ThreadLocal 換成 InheritableThreadLocal,如下:

publicfinalclassContext{
privatestaticfinalInheritableThreadLocalLOCAL=newInheritableThreadLocal(){
protectedContextinitialValue(){
returnnewContext();
}
};

publicstaticContextgetContext(){
return(Context)LOCAL.get();
}
}

newThread(newRunnable(){
@Override
publicvoidrun(){
//此時的clientInfo能正常獲取到
MapclientInfo=Context.getContext().getClientInfo();
Stringversion=clientInfo.get("version");
//以下正常邏輯
....
}
}).start();

為什么 InheritableThreadLocal 能有這么神奇,背后的原理是什么?

由前文介紹我們得知,ThreadLocal 變量最終是存在 ThreadLocalMap 中的。

那么能否在創建新線程的時候,把當前線程的 ThreadLocalMap 復制給新線程的 ThreadLocalMap 呢?

這樣的話即便你從新線程中調用 threadlocal.get 也照樣能獲得對應的本地變量,和 InheritableThreadLocal 相關的底層干的就是這個事。

我們先來瞧一瞧 InheritableThreadLocal 長啥樣:

publicclassInheritableThreadLocal<T>extendsThreadLocal<T>{

ThreadLocalMapgetMap(Threadt){
returnt.inheritableThreadLocals;
}

voidcreateMap(Threadt,TfirstValue){
t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);
}
}

由此可知 InheritableThreadLocal 其實是繼承自 ThreadLocal 類的。

此外我們在 getMap 和 createMap 這兩個方法中也發現它的底層其實是用 inheritableThreadLocals 來存儲的,而 ThreadLocal 用的是 threadLocals 變量存儲的。

publicclassThreadimplementsRunnable{
//ThreadLocal實例的底層存儲
ThreadLocal.ThreadLocalMapthreadLocals=null;

//inheritableThreadLocals實例的底層存儲
ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;
}

知道了這些,我們再來看下創建線程時涉及到的 inheritableThreadLocals 復制相關的關鍵代碼如下:

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)
//將當前線程的inheritableThreadLocals復制給新創建線程的inheritableThreadLocals
this.inheritableThreadLocals=
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
}

由此可知,在創建新線程時,在初始化時其實相關邏輯是幫我們干了復制 inheritableThreadLocals 的操作,至此真相大白!

總結

看完本文,相信大家對 Threadlocal 與 InheritableThreadLocal 的使用及其底層原理的掌握已不存在疑問。

這也提醒我們熟練地掌握一個組件或一項技術最好的方式還是熟讀它的源碼,畢竟源碼之下無秘密。

當我們使用到別人封裝好的組件或類時,如果有興趣也可以也看一下它的源碼。

以本文為例,其實我們工程中多處地方都使用了 Context.getContext().getClientInfo();這樣的獲取客戶端信息的形式,用慣了導致在多線程環境下沒有引起警惕,以致踩了坑。

另外需要注意的是 ThreadLocal 使用不當可能導致內存泄漏,需要在線程結束后及時 remove 掉,這些技術細節不是本文重點,故而沒有深入詳解,有興趣的大家可以去查閱相關資料

歷史好文:

多個線程為了同個資源打起架來了,該如何讓他們安分?

美團三面:一直追問我, MySQL 幻讀被徹底解決了嗎?

原來墻,是這么把我 TCP 連接干掉的!

面試官:你確定 Redis 是單線程的進程嗎?

字節一面:HTTPS 一定安全可靠嗎?


審核編輯 :李倩


聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 多線程
    +關注

    關注

    0

    文章

    278

    瀏覽量

    19943
  • 代碼
    +關注

    關注

    30

    文章

    4780

    瀏覽量

    68527
  • 變量
    +關注

    關注

    0

    文章

    613

    瀏覽量

    28360

原文標題:多線程引發的慘案!直接把年終給干沒了

文章出處:【微信號:小林coding,微信公眾號:小林coding】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    Java多線程的用法

    本文將介紹一下Java多線程的用法。 基礎介紹 什么是多線程 指的是在一個進程中同時運行多個線程,每個線程都可以獨立執行不同的任務或操作。 與單線程
    的頭像 發表于 09-30 17:07 ?947次閱讀

    多線程技術在串口通信中的應用

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

    多線程與聊天室程序的創建

    多線程程序的編寫,多線程應用中容易出現的問題。互斥對象的講解,如何采用互斥對象來實現多線程的同步。如何利用命名互斥對象保證應用程序只有一個實例運行。應用多線程編寫網絡聊天室程序。
    發表于 05-16 15:22 ?0次下載

    設計多線程和多核系統

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

    linux多線程編程技術

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

    多線程好還是單線程好?單線程多線程的區別 優缺點分析

    摘要:如今單線程多線程已經得到普遍運用,那么到底多線程好還是單線程好呢?單線程多線程的區別又
    發表于 12-08 09:33 ?8.1w次閱讀

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

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

    什么是多線程編程?多線程編程基礎知識

    摘要:多線程編程是現代軟件技術中很重要的一個環節。要弄懂多線程,這就要牽涉到多進程。本文主要以多線程編程以及多線程編程相關知識而做出的一些結論。
    發表于 12-08 16:30 ?1.3w次閱讀

    Linux下的多線程編程

    1 引言  線程(thread)技術早在60年代就被提出,但真正應用多線程到操作系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支持線程的概念,但是在一個
    發表于 04-02 14:43 ?604次閱讀

    SpringBoot實現多線程

    SpringBoot實現多線程
    的頭像 發表于 01-12 16:59 ?1814次閱讀
    SpringBoot實現<b class='flag-5'>多線程</b>

    labview AMC多線程

    labview_AMC多線程
    發表于 08-21 10:31 ?32次下載

    多線程idm下載軟件

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

    多線程如何保證數據的同步

    多線程編程是一種并發編程的方法,意味著程序中同時運行多個線程,每個線程可獨立執行不同的任務,共享同一份數據。由于多線程并發執行的特點,會引發
    的頭像 發表于 11-17 14:22 ?1222次閱讀

    mfc多線程編程實例

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

    socket 多線程編程實現方法

    在現代網絡編程中,多線程技術被廣泛應用于提高服務器的并發處理能力。Socket編程是網絡通信的基礎,而將多線程技術應用于Socket編程,可以顯著提升服務器的性能。 多線程編程的基本概念 多線
    的頭像 發表于 11-12 14:16 ?339次閱讀
    主站蜘蛛池模板: 国产亚洲精品99一区二区 | 亚洲精品第二页| 有码在线播放| 啊灬啊灬啊灬快灬深高潮啦| 国产伦精品一区二区免费| 久久九九日本韩国精品| 欧美片第1页 综合| 亚洲精品第二页| 北原多香子qvod| 久久精品观看| 国产精品久久毛片A片软件爽爽| 九九热国产视频| 日韩精品一区二区中文| 一级毛片西西人体44rt高清| 大胸美女被c| 久久在精品线影院| 暖暖 免费 高清 日本 在线| 小短文H啪纯肉公交车| 91夫妻交友论坛| 国产一区二区青青精品久久| 欧美人与动牲交A免费| 亚洲在线成色综合网站| 国产99视频精品一区| 免费国产成人| 亚洲天堂视频网站| 国产高清免费观看| 火影忍者高清无码黄漫| 日本一区不卡在线播放视频免费| 用快播看黄的网站| 国产美女又黄又爽又色视频网站| 久久re视频这里精品一本到99| 日夜啪啪一区二区三区| 99精品国产免费观看视频| 精品亚洲一区二区在线播放| 年轻的老师5理伦片| 亚洲三级视频在线| 国产精品久久久久无码AV色戒| 殴美黄色网| 亚洲精品AV无码永久无码| 大地影院免费观看视频| 欧美高清videos 360p|