Java線程池中多余的線程是如何回收的
最近閱讀了JDK線程池ThreadPoolExecutor的源碼,對線程池執(zhí)行任務(wù)的流程有了大體了解,實(shí)際上這個流程也十分通俗易懂,就不再贅述了,別人寫的比我好多了。
不過,我倒是對線程池是如何回收工作線程比較感興趣,所以簡單分析了一下,加深對線程池的理解吧。
那么,就以JDK1.8為例分析吧。
1.runWorker(Worker w)
工作線程啟動后,就進(jìn)入runWorker(Worker w)方法。
里面是一個while循環(huán),循環(huán)判斷任務(wù)是否為空,若不為空,執(zhí)行任務(wù);若取不到任務(wù),或發(fā)生異常,退出循環(huán),執(zhí)行processWorkerExit(w, completedAbruptly); 在這個方法里把工作線程移除掉。
取任務(wù)的來源有兩個,一個是firstTask,這個是工作線程第一次跑的時候執(zhí)行的任務(wù),最多只能執(zhí)行一次,后面得從getTask()方法里取任務(wù)??磥恚琯etTask()是關(guān)鍵,在不考慮異常的場景下,返回null,就表示退出循環(huán),結(jié)束線程。下一步,就得看看,什么情況下getTask()會返回null。
(篇幅有限,分段截取,省略中間執(zhí)行任務(wù)的步驟)
2. getTask() 返回null
一共有兩種情況會返回null,見紅框處 。
第一種情況,線程池的狀態(tài)已經(jīng)是STOP,TIDYING, TERMINATED,或者是SHUTDOWN且工作隊(duì)列為空;
第二種情況,工作線程數(shù)已經(jīng)大于最大線程數(shù)或當(dāng)前工作線程已超時,且,還有其他工作線程或任務(wù)隊(duì)列為空。這點(diǎn)比較難理解,總之先記住,后面會用。
下面以條件1和條件2分別指代這兩種情況的判斷條件。
3. 分場景分析線程池回收工作線程
3.1 未調(diào)用shutdown() ,RUNNING狀態(tài)下全部任務(wù)執(zhí)行完成的場景
這種場景,會將工作線程的數(shù)量減少到核心線程數(shù)大?。ㄈ绻緛砭蜎]有超過,則不需要回收)。
比如一個線程池,核心線程數(shù)為4,最大線程數(shù)為8。一開始是4個工作線程,當(dāng)任務(wù)把任務(wù)隊(duì)列塞滿,就得將工作線程增加到8. 當(dāng)后面任務(wù)執(zhí)行到差不多了,線程取不到任務(wù)了,就會回收到4個工作線程的狀態(tài)(取決于allowCoreThreadTimeOut的值,這里討論默認(rèn)值false的情況,即核心線程不會超時。如果為true,工作線程可以全部銷毀)。
可以先排除上面提到的條件1,線程池的狀態(tài)已經(jīng)是STOP,TIDYING, TERMINATED,或者是SHUTDOWN且工作隊(duì)列為空。因?yàn)榫€程池一直是RUNNING,這條判斷永遠(yuǎn)是false。在這個場景中,可以當(dāng)條件1不存在。
下面分析取不出任務(wù)時線程是怎么運(yùn)行的。
step1. 從任務(wù)隊(duì)列取任務(wù)有兩種方式,超時等待還是可以一直阻塞下去。決定因素是timed變量。該變量在前面賦值,如果當(dāng)前線程數(shù)大于核心線程數(shù),變量timed為true, 否則為false(上面說了,這里只討論allowCoreThreadTimeOut為false的情況)。很明顯,現(xiàn)在討論的是timed為true的情況。keepAliveTime一般不設(shè)置,默認(rèn)值為0,所以基本上可以認(rèn)為是不阻塞,馬上返回取任務(wù)的結(jié)果。
在線程超時等待喚醒之后,發(fā)現(xiàn)取不出任務(wù),timeOut變?yōu)閠rue,進(jìn)入下一次循環(huán)。
step2. 來到條件1的判斷,線程池一直RUNNING, 不進(jìn)入代碼塊。
step3. 來到條件2的判斷,這時任務(wù)隊(duì)列為空,條件成立,CAS減少線程數(shù),若成功,返回null,否則,重復(fù)step1。
這里要注意,有可能多條線程同時通過條件2的判斷,那會不會減少后線程的數(shù)量反而比預(yù)想的核心線程數(shù)少呢?
比如當(dāng)前線程數(shù)已經(jīng)只有5條了,此時有兩條線程同時喚醒,通過條件2的判斷,同時減少數(shù)量,那剩下的線程數(shù)反而只有3條,和預(yù)期不一致。
實(shí)際上是不會的。為了防止這種情況,compareAndDecrementWorkerCount(c) 用的是CAS方法,如果CAS失敗就continue,進(jìn)入下一輪循環(huán),重新判斷。
像上述例子,其中一條線程會CAS失敗,然后重新進(jìn)入循環(huán),發(fā)現(xiàn)工作線程數(shù)已經(jīng)只有4了,timed為false, 這條線程就不會被銷毀,可以一直阻塞了(workQueue.take())。
這一點(diǎn)我思考了很久才得出答案,一直在想沒有加鎖的情況下是怎么保證一定能不多不少回收到核心線程數(shù)的呢。原來是CAS的奧妙。
從這里也可以看出,雖然有核心線程數(shù),但線程并沒有區(qū)分是核心還是非核心,并不是先創(chuàng)建的就是核心,超過核心線程數(shù)后創(chuàng)建的就是非核心,最終保留哪些線程,完全隨機(jī)。
3.2 調(diào)用shutdown() ,全部任務(wù)執(zhí)行完成的場景
這種場景,無論是核心線程還是非核心線程,所有工作線程都會被銷毀。
在調(diào)用shutdown()之后,會向所有的空閑工作線程發(fā)送中斷信號。
最終傳入false,調(diào)用下面這個方法。
可以看出,在發(fā)出中斷信號前,會判斷是否已經(jīng)中斷,以及要獲得工作線程的獨(dú)占鎖。
發(fā)出中斷信號的時候,工作線程要么在getTask()里準(zhǔn)備獲取任務(wù),要么在執(zhí)行任務(wù),那就得等它執(zhí)行完當(dāng)前任務(wù)才會發(fā)出,因?yàn)楣ぷ骶€程在執(zhí)行任務(wù)的時候,也會工作線程加鎖。工作線程執(zhí)行完任務(wù),又跑到getTask()里面去了。
所以我們只要看getTask()里面怎么應(yīng)對中斷異常的就可以了。
工作線程在getTask()里,有兩種可能。
3.2.1 任務(wù)已全部完成,線程在阻塞等待。
很簡單,中斷信號將其喚醒,從而進(jìn)入下一輪循環(huán)。到達(dá)條件1處,符合條件,減少工作線程數(shù)量,并返回null,由外層結(jié)束這條線程。
這里的decrementWorkerCount()是自旋式的,一定會減1。
3.2.2 任務(wù)還沒有完全執(zhí)行完
調(diào)用shutdown()之后,未執(zhí)行完的任務(wù)要執(zhí)行完畢,池子才能結(jié)束。所以此時有可能線程還在工作。
這里又要分兩個階段討論
階段1 任務(wù)較多,工作線程都能獲得任務(wù)
這里還不涉及到線程退出,可以跳過不看,只是分析一下收到中斷信號后線程的表現(xiàn)。
假設(shè)有線程A,正通過getTask()里獲取任務(wù)。此時A被中斷,在獲取任務(wù)時,無論是poll()還是take(),都會拋出中斷異常。異常被捕獲,重新進(jìn)入下一輪循環(huán),只要隊(duì)列不為空,就可以繼續(xù)取任務(wù)。
線程A被中斷,再次取任務(wù),調(diào)用workQueue.poll() or workQueue.take(),不會拋出異常嗎?還可以正常取出任務(wù)嗎?
這就要看workQueue的實(shí)現(xiàn)了。workQueue是BlockingQueue類型,以常見的LinkedBlockingQueue和ArrayBlockingQueue為例,加鎖時都是調(diào)用lockInterruptibly(),是響應(yīng)中斷的。該方法又調(diào)用了AQS的acquireInterruptibly(int arg)。
acquireInterruptibly(int arg),無論是在入口處判斷中斷異常,還是在parkAndCheckInterrupt()方法阻塞,被中斷喚醒并判斷中斷異常時,均使用了Thread.interrupted()。這個方法會返回線程的中斷狀態(tài),并把中斷狀態(tài)重置!也就是說,線程不再是中斷狀態(tài)了,這樣在再次取任務(wù)時,就不會報(bào)錯了。
因此,這對于正在準(zhǔn)備取任務(wù)的線程,只是相當(dāng)于浪費(fèi)了一次循環(huán),這可能是線程中斷帶來的副作用吧,當(dāng)然,對整體的運(yùn)行不影響。
分析到這里,我不禁感嘆,這里BlockingQueue剛好是會重置中斷狀態(tài),這到底是怎么想出來的絕妙設(shè)計(jì)?。緿oug Lea大神Orz.
階段2 任務(wù)剛好要執(zhí)行完了
這時任務(wù)已經(jīng)快取完了,比如有4條工作線程,只剩下2個任務(wù),那就可能出現(xiàn)2條線程獲得任務(wù),2條線程阻塞。
因?yàn)樵讷@取任務(wù)前的判斷,沒有加鎖,那么會不會出現(xiàn),所有線程都通過了前面的校驗(yàn),來到workQueue獲取任務(wù)的地方,剛好任務(wù)隊(duì)列已經(jīng)空了,線程全部阻塞了呢?因?yàn)閟hutdown() 已經(jīng)執(zhí)行完畢,無法再向線程發(fā)出中斷信號,從而線程一直在阻塞,無法被回收。
這種是不會發(fā)生的。
假設(shè)有A,B,C,D四條工作線程,同時通過了條件1和條件2的判斷,來到取任務(wù)的地方。那么,工作隊(duì)列至少還有一個任務(wù),至少會有一條線程能取到任務(wù)。
假設(shè)A,B獲得了任務(wù),C,D阻塞。
A, B接下來的步驟是:
step1.任務(wù)執(zhí)行完成后,再次getTask(),此時符合條件1,返回null,線程準(zhǔn)備被回收。
step2.processWorkerExit(Worker w, boolean completedAbruptly) 將線程回收。
回收就只是把線程干掉這么簡單嗎?來看看processWorkerExit(Worker w, boolean completedAbruptly) 的方法。
可以看到,在里面除了workers.remove(w) 移除線,還調(diào)用了tryTerminate()。
第一個判斷條件沒有一個子條件符合,跳過。第二個條件,工作線程還存在,那么隨機(jī)中斷一條空閑線程。
那么問題就來了,中斷一條空閑線程,也沒說是一定中斷正在阻塞的線程啊。如果A, B同時退出,有沒有可能出現(xiàn)A中斷B, B中斷A,AB互相中斷,從而沒有線程去中斷喚醒阻塞的線程呢?
答案仍然是,想多了……
假設(shè)A能走到這里,說明A已經(jīng)從工作線程的集合workers里面移除了(processWorkerExit(Worker w, boolean completedAbruptly) 在tryTerminate()之前,已經(jīng)將其移除)。那么A中斷B,B來到這里中斷,就不會在workers里面找到A了。
也就是說,退出的線程不能互相中斷,我從集合中退出后,中斷了你,你不能中斷我,因?yàn)槲乙呀?jīng)退出集合,你只能中斷別人。那么,即使有N個線程同時退出,至少在最后,也會有一條線程,會中斷剩余的阻塞線程。
就像多米諾骨牌一樣,中斷信號就會被傳播下去。
阻塞的C,D中的任意一條被中斷喚醒后,又會重復(fù)step1的動作,周而復(fù)始,直到所有阻塞線程都被中斷,喚醒。
這也是為什么在tryTerminate()里面,傳入false,只需要中斷任意一條空閑線程的原因。
想到這里,再次對Doug Lea心生欽敬(粵語)之情。這設(shè)計(jì)得也太妙了叭。
4. 總結(jié)
ThreadPoolExecutor回收工作線程,一條線程getTask()返回null,就會被回收。
分兩種場景。
1) 未調(diào)用shutdown() ,RUNNING狀態(tài)下全部任務(wù)執(zhí)行完成的場景
線程數(shù)量大于corePoolSize,線程超時阻塞,超時喚醒后CAS減少工作線程數(shù),如果CAS成功,返回null,線程回收。否則進(jìn)入下一次循環(huán)。當(dāng)工作者線程數(shù)量小于等于corePoolSize,就可以一直阻塞了。
2) 調(diào)用shutdown() ,全部任務(wù)執(zhí)行完成的場景
shutdown() 會向所有線程發(fā)出中斷信號,這時有兩種可能。
2.1)所有線程都在阻塞
中斷喚醒,進(jìn)入循環(huán),都符合第一個if判斷條件,都返回null,所有線程回收。
2.2)任務(wù)還沒有完全執(zhí)行完
至少會有一條線程被回收。在processWorkerExit(Worker w, boolean completedAbruptly)方法里會調(diào)用tryTerminate(),向任意空閑線程發(fā)出中斷信號。所有被阻塞的線程,最終都會被一個個喚醒,回收。
這一次的分析,昨晚開始寫,寫到一半卡殼,今天早上接著寫,前后花了大概2+2=4個小時寫博客以及1小時思考。
說實(shí)話自己還是有點(diǎn)亂,無法一下子理解透徹,也不知道自己理解得對不對。
有沒有用,我也不知道,只能說,加深了對線程池的理解吧(安慰自己),同時也感慨設(shè)計(jì)之精妙。
到此這篇關(guān)于Java線程池中多余的線程是如何回收的的文章就介紹到這了,更多相關(guān)Java 多余線程回收內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot表單提交之validator校驗(yàn)
在前臺表單驗(yàn)證的時候,通常會校驗(yàn)一些數(shù)據(jù)的可行性,比如是否為空,長度,身份證,郵箱等等,這篇文章主要給大家介紹了關(guān)于springboot表單提交之validator校驗(yàn)的相關(guān)資料,需要的朋友可以參考下2021-05-05Spring cloud Gateway簡介及相關(guān)配置方法
這篇文章主要介紹了Spring cloud Gateway簡介及相關(guān)配置方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04IntelliJ?IDEA?2022.2.3最新激活圖文教程(親測有用永久激活)
今天給大家分享一個?IDEA?2022.2.3?的激活破解教程,全文通過文字+圖片的方式講解,手把手教你如何激活破解?IDEA,?只需要幾分鐘即可搞定,對idea2022.2.3激活碼感興趣的朋友跟隨小編一起看看吧2022-11-11