<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          今天來(lái)了一位妹紙面試,我問(wèn)她:線程池中多余的線程是如何回收的?她有點(diǎn)懵~

          共 5110字,需瀏覽 11分鐘

           ·

          2021-03-07 12:24

          上一篇:再見(jiàn)了,收費(fèi)的Navicat
          來(lái)源:r6d.cn/wkgk


          最近,作為公司的面試官,問(wèn)了不少求職者關(guān)于線程池的問(wèn)題,能回答清楚的還是很少,這里結(jié)合JDK線程池ThreadPoolExecutor的源碼來(lái)寫(xiě)一篇文章,幫助大家梳理一下對(duì)線程池執(zhí)行任務(wù)的流程的理解,實(shí)際上這個(gè)流程也十分通俗易懂,就不再贅述了~


          不過(guò),我倒是對(duì)線程池是如何回收工作線程比較感興趣,所以簡(jiǎn)單分析了一下,加深對(duì)線程池的理解吧。


          下面以JDK1.8為例進(jìn)行分析


          1. runWorker(Worker w)


          工作線程啟動(dòng)后,就進(jìn)入runWorker(Worker w)方法。


          里面是一個(gè)while循環(huán),循環(huán)判斷任務(wù)是否為空,若不為空,執(zhí)行任務(wù);若取不到任務(wù),或發(fā)生異常,退出循環(huán),執(zhí)行processWorkerExit(w, completedAbruptly); 在這個(gè)方法里把工作線程移除掉。


          取任務(wù)的來(lái)源有兩個(gè),一個(gè)是firstTask,這個(gè)是工作線程第一次跑的時(shí)候執(zhí)行的任務(wù),最多只能執(zhí)行一次,后面得從getTask()方法里取任務(wù)??磥?lái),getTask()是關(guān)鍵,在不考慮異常的場(chǎng)景下,返回null,就表示退出循環(huán),結(jié)束線程。下一步,就得看看,什么情況下getTask()會(huì)返回null。


          (篇幅有限,分段截取,省略中間執(zhí)行任務(wù)的步驟)




          2. getTask() 返回null


          一共有兩種情況會(huì)返回null,見(jiàn)紅框處 。


          第一種情況,線程池的狀態(tài)已經(jīng)是STOP,TIDYING, TERMINATED,或者是SHUTDOWN且工作隊(duì)列為空;


          第二種情況,工作線程數(shù)已經(jīng)大于最大線程數(shù)或當(dāng)前工作線程已超時(shí),且,還有其他工作線程或任務(wù)隊(duì)列為空。這點(diǎn)比較難理解,總之先記住,后面會(huì)用。


          下面以條件1和條件2分別指代這兩種情況的判斷條件。



          3. 分場(chǎng)景分析線程池回收工作線程


          3.1 未調(diào)用shutdown() ,RUNNING狀態(tài)下全部任務(wù)執(zhí)行完成的場(chǎng)景


          這種場(chǎng)景,會(huì)將工作線程的數(shù)量減少到核心線程數(shù)大?。ㄈ绻緛?lái)就沒(méi)有超過(guò),則不需要回收)。


          比如一個(gè)線程池,核心線程數(shù)為4,最大線程數(shù)為8。一開(kāi)始是4個(gè)工作線程,當(dāng)任務(wù)把任務(wù)隊(duì)列塞滿,就得將工作線程增加到8. 當(dāng)后面任務(wù)執(zhí)行到差不多了,線程取不到任務(wù)了,就會(huì)回收到4個(gè)工作線程的狀態(tài)(取決于allowCoreThreadTimeOut的值,這里討論默認(rèn)值false的情況,即核心線程不會(huì)超時(shí)。如果為true,工作線程可以全部銷毀)。


          可以先排除上面提到的條件1,線程池的狀態(tài)已經(jīng)是STOP,TIDYING, TERMINATED,或者是SHUTDOWN且工作隊(duì)列為空。因?yàn)榫€程池一直是RUNNING,這條判斷永遠(yuǎn)是false。在這個(gè)場(chǎng)景中,可以當(dāng)條件1不存在。


          下面分析取不出任務(wù)時(shí)線程是怎么運(yùn)行的。


          step1. 從任務(wù)隊(duì)列取任務(wù)有兩種方式,超時(shí)等待還是可以一直阻塞下去。決定因素是timed變量。該變量在前面賦值,如果當(dāng)前線程數(shù)大于核心線程數(shù),變量timed為true, 否則為false(上面說(shuō)了,這里只討論allowCoreThreadTimeOut為false的情況)。很明顯,現(xiàn)在討論的是timed為true的情況。keepAliveTime一般不設(shè)置,默認(rèn)值為0,所以基本上可以認(rèn)為是不阻塞,馬上返回取任務(wù)的結(jié)果。


          在線程超時(shí)等待喚醒之后,發(fā)現(xiàn)取不出任務(wù),timeOut變?yōu)閠rue,進(jìn)入下一次循環(huán)。


          step2. 來(lái)到條件1的判斷,線程池一直RUNNING, 不進(jìn)入代碼塊。


          step3. 來(lái)到條件2的判斷,這時(shí)任務(wù)隊(duì)列為空,條件成立,CAS減少線程數(shù),若成功,返回null,否則,重復(fù)step1。


          這里要注意,有可能多條線程同時(shí)通過(guò)條件2的判斷,那會(huì)不會(huì)減少后線程的數(shù)量反而比預(yù)想的核心線程數(shù)少呢?


          比如當(dāng)前線程數(shù)已經(jīng)只有5條了,此時(shí)有兩條線程同時(shí)喚醒,通過(guò)條件2的判斷,同時(shí)減少數(shù)量,那剩下的線程數(shù)反而只有3條,和預(yù)期不一致。


          實(shí)際上是不會(huì)的。為了防止這種情況,compareAndDecrementWorkerCount(c) 用的是CAS方法,如果CAS失敗就continue,進(jìn)入下一輪循環(huán),重新判斷。


          像上述例子,其中一條線程會(huì)CAS失敗,然后重新進(jìn)入循環(huán),發(fā)現(xiàn)工作線程數(shù)已經(jīng)只有4了,timed為false, 這條線程就不會(huì)被銷毀,可以一直阻塞了(workQueue.take())。


          這一點(diǎn)我思考了很久才得出答案,一直在想沒(méi)有加鎖的情況下是怎么保證一定能不多不少回收到核心線程數(shù)的呢。原來(lái)是CAS的奧妙。


          從這里也可以看出,雖然有核心線程數(shù),但線程并沒(méi)有區(qū)分是核心還是非核心,并不是先創(chuàng)建的就是核心,超過(guò)核心線程數(shù)后創(chuàng)建的就是非核心,最終保留哪些線程,完全隨機(jī)。


          3.2 調(diào)用shutdown() ,全部任務(wù)執(zhí)行完成的場(chǎng)景


          這種場(chǎng)景,無(wú)論是核心線程還是非核心線程,所有工作線程都會(huì)被銷毀。

          在調(diào)用shutdown()之后,會(huì)向所有的空閑工作線程發(fā)送中斷信號(hào)。



          最終傳入false,調(diào)用下面這個(gè)方法。


          可以看出,在發(fā)出中斷信號(hào)前,會(huì)判斷是否已經(jīng)中斷,以及要獲得工作線程的獨(dú)占鎖。


          發(fā)出中斷信號(hào)的時(shí)候,工作線程要么在getTask()里準(zhǔn)備獲取任務(wù),要么在執(zhí)行任務(wù),那就得等它執(zhí)行完當(dāng)前任務(wù)才會(huì)發(fā)出,因?yàn)楣ぷ骶€程在執(zhí)行任務(wù)的時(shí)候,也會(huì)工作線程加鎖。工作線程執(zhí)行完任務(wù),又跑到getTask()里面去了。


          所以我們只要看getTask()里面怎么應(yīng)對(duì)中斷異常的就可以了。


          工作線程在getTask()里,有兩種可能。


          3.2.1 任務(wù)已全部完成,線程在阻塞等待。


          很簡(jiǎn)單,中斷信號(hào)將其喚醒,從而進(jìn)入下一輪循環(huán)。到達(dá)條件1處,符合條件,減少工作線程數(shù)量,并返回null,由外層結(jié)束這條線程。


          這里的decrementWorkerCount()是自旋式的,一定會(huì)減1。



          3.2.2 任務(wù)還沒(méi)有完全執(zhí)行完


          調(diào)用shutdown()之后,未執(zhí)行完的任務(wù)要執(zhí)行完畢,池子才能結(jié)束。所以此時(shí)有可能線程還在工作。


          這里又要分兩個(gè)階段討論


          階段1 任務(wù)較多,工作線程都能獲得任務(wù)


          這里還不涉及到線程退出,可以跳過(guò)不看,只是分析一下收到中斷信號(hào)后線程的表現(xiàn)。


          假設(shè)有線程A,正通過(guò)getTask()里獲取任務(wù)。此時(shí)A被中斷,在獲取任務(wù)時(shí),無(wú)論是poll()還是take(),都會(huì)拋出中斷異常。異常被捕獲,重新進(jìn)入下一輪循環(huán),只要隊(duì)列不為空,就可以繼續(xù)取任務(wù)。


          線程A被中斷,再次取任務(wù),調(diào)用workQueue.poll() or workQueue.take(),不會(huì)拋出異常嗎?還可以正常取出任務(wù)嗎?


          這就要看workQueue的實(shí)現(xiàn)了。workQueue是BlockingQueue類型,以常見(jiàn)的LinkedBlockingQueue和ArrayBlockingQueue為例,加鎖時(shí)都是調(diào)用lockInterruptibly(),是響應(yīng)中斷的。該方法又調(diào)用了AQS的acquireInterruptibly(int arg)。


          acquireInterruptibly(int arg),無(wú)論是在入口處判斷中斷異常,還是在parkAndCheckInterrupt()方法阻塞,被中斷喚醒并判斷中斷異常時(shí),均使用了Thread.interrupted()。這個(gè)方法會(huì)返回線程的中斷狀態(tài),并把中斷狀態(tài)重置!也就是說(shuō),線程不再是中斷狀態(tài)了,這樣在再次取任務(wù)時(shí),就不會(huì)報(bào)錯(cuò)了。


          因此,這對(duì)于正在準(zhǔn)備取任務(wù)的線程,只是相當(dāng)于浪費(fèi)了一次循環(huán),這可能是線程中斷帶來(lái)的副作用吧,當(dāng)然,對(duì)整體的運(yùn)行不影響。


          分析到這里,我不禁感嘆,這里BlockingQueue剛好是會(huì)重置中斷狀態(tài),這到底是怎么想出來(lái)的絕妙設(shè)計(jì)???Doug Lea大神Orz.



          階段2 任務(wù)剛好要執(zhí)行完了


          這時(shí)任務(wù)已經(jīng)快取完了,比如有4條工作線程,只剩下2個(gè)任務(wù),那就可能出現(xiàn)2條線程獲得任務(wù),2條線程阻塞。


          因?yàn)樵讷@取任務(wù)前的判斷,沒(méi)有加鎖,那么會(huì)不會(huì)出現(xiàn),所有線程都通過(guò)了前面的校驗(yàn),來(lái)到workQueue獲取任務(wù)的地方,剛好任務(wù)隊(duì)列已經(jīng)空了,線程全部阻塞了呢?因?yàn)閟hutdown() 已經(jīng)執(zhí)行完畢,無(wú)法再向線程發(fā)出中斷信號(hào),從而線程一直在阻塞,無(wú)法被回收。


          這種是不會(huì)發(fā)生的。


          假設(shè)有A,B,C,D四條工作線程,同時(shí)通過(guò)了條件1和條件2的判斷,來(lái)到取任務(wù)的地方。那么,工作隊(duì)列至少還有一個(gè)任務(wù),至少會(huì)有一條線程能取到任務(wù)。


          假設(shè)A,B獲得了任務(wù),C,D阻塞。


          A, B接下來(lái)的步驟是:


          step1.任務(wù)執(zhí)行完成后,再次getTask(),此時(shí)符合條件1,返回null,線程準(zhǔn)備被回收。


          step2.processWorkerExit(Worker w, boolean completedAbruptly) 將線程回收。


          回收就只是把線程干掉這么簡(jiǎn)單嗎?來(lái)看看processWorkerExit(Worker w, boolean completedAbruptly) 的方法。



          可以看到,在里面除了workers.remove(w) 移除線,還調(diào)用了tryTerminate()。



          第一個(gè)判斷條件沒(méi)有一個(gè)子條件符合,跳過(guò)。第二個(gè)條件,工作線程還存在,那么隨機(jī)中斷一條空閑線程。


          那么問(wèn)題就來(lái)了,中斷一條空閑線程,也沒(méi)說(shuō)是一定中斷正在阻塞的線程啊。如果A, B同時(shí)退出,有沒(méi)有可能出現(xiàn)A中斷B, B中斷A,AB互相中斷,從而沒(méi)有線程去中斷喚醒阻塞的線程呢?


          答案仍然是,想多了……


          假設(shè)A能走到這里,說(shuō)明A已經(jīng)從工作線程的集合workers里面移除了(processWorkerExit(Worker w, boolean completedAbruptly) 在tryTerminate()之前,已經(jīng)將其移除)。那么A中斷B,B來(lái)到這里中斷,就不會(huì)在workers里面找到A了。



          也就是說(shuō),退出的線程不能互相中斷,我從集合中退出后,中斷了你,你不能中斷我,因?yàn)槲乙呀?jīng)退出集合,你只能中斷別人。那么,即使有N個(gè)線程同時(shí)退出,至少在最后,也會(huì)有一條線程,會(huì)中斷剩余的阻塞線程。


          就像多米諾骨牌一樣,中斷信號(hào)就會(huì)被傳播下去。


          阻塞的C,D中的任意一條被中斷喚醒后,又會(huì)重復(fù)step1的動(dòng)作,周而復(fù)始,直到所有阻塞線程都被中斷,喚醒。


          這也是為什么在tryTerminate()里面,傳入false,只需要中斷任意一條空閑線程的原因。


          想到這里,再次對(duì)Doug Lea心生欽敬(粵語(yǔ))之情。這設(shè)計(jì)得也太妙了叭。


          4. 總結(jié)


          ThreadPoolExecutor回收工作線程,一條線程getTask()返回null,就會(huì)被回收。

          分兩種場(chǎng)景。


          1、未調(diào)用shutdown() ,RUNNING狀態(tài)下全部任務(wù)執(zhí)行完成的場(chǎng)景

          線程數(shù)量大于corePoolSize,線程超時(shí)阻塞,超時(shí)喚醒后CAS減少工作線程數(shù),如果CAS成功,返回null,線程回收。否則進(jìn)入下一次循環(huán)。當(dāng)工作者線程數(shù)量小于等于corePoolSize,就可以一直阻塞了。


          2、調(diào)用shutdown() ,全部任務(wù)執(zhí)行完成的場(chǎng)景

          shutdown() 會(huì)向所有線程發(fā)出中斷信號(hào),這時(shí)有兩種可能。


          2.1)所有線程都在阻塞


          中斷喚醒,進(jìn)入循環(huán),都符合第一個(gè)if判斷條件,都返回null,所有線程回收。


          2.2)任務(wù)還沒(méi)有完全執(zhí)行完


          至少會(huì)有一條線程被回收。在processWorkerExit(Worker w, boolean completedAbruptly)方法里會(huì)調(diào)用tryTerminate(),向任意空閑線程發(fā)出中斷信號(hào)。所有被阻塞的線程,最終都會(huì)被一個(gè)個(gè)喚醒,回收。


          PS:如果覺(jué)得我的分享不錯(cuò),歡迎大家隨手點(diǎn)贊、在看。

          大家一起在評(píng)論區(qū)聊聊唄~


          關(guān)注微信公眾號(hào):互聯(lián)網(wǎng)架構(gòu)師,在后臺(tái)回復(fù):2T,可以獲取我整理的教程,都是干貨。


          猜你喜歡

          1、GitHub 標(biāo)星 3.2w!史上最全技術(shù)人員面試手冊(cè)!FackBoo發(fā)起和總結(jié)

          2、如何才能成為優(yōu)秀的架構(gòu)師?

          3、從零開(kāi)始搭建創(chuàng)業(yè)公司后臺(tái)技術(shù)棧

          4、程序員一般可以從什么平臺(tái)接私活?

          5、37歲程序員被裁,120天沒(méi)找到工作,無(wú)奈去小公司,結(jié)果懵了...

          6、滴滴業(yè)務(wù)中臺(tái)構(gòu)建實(shí)踐,首次曝光

          7、不認(rèn)命,從10年流水線工人,到谷歌上班的程序媛,一位湖南妹子的勵(lì)志故事

          8、15張圖看懂瞎忙和高效的區(qū)別

          9、2T架構(gòu)師學(xué)習(xí)資料干貨分享

          瀏覽 63
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久久国产精品黄毛片 | 丰满少妇奶头出奶水 | 影音先锋av无码在线 | 亚洲综合视频在线观看 | 日本午夜福利片 |