看看提交任務到線程池的源碼執(zhí)行流程是什么樣子的
? 我們在上一節(jié)中對線程池構(gòu)造的核心參數(shù)進行了細致的分析,一步一圖的對每個參數(shù)的含義、作用都做了圖文并茂的分析,相信你已經(jīng)掌握的很好了。那么本節(jié)我們就繼續(xù)深入源碼和原理,來一起看看任務提交到線程之后的源碼執(zhí)行流程具體是什么樣子的。
? 按照慣例,我們先看一張任務執(zhí)行的完整流程如圖1所示。

圖1
?我們根據(jù)這張圖中的主要節(jié)點,一步一步結(jié)合源碼來分析一下。
任務提交
????首先是任務提交,線程池任務提交有兩種方式,execute()和submit()。首先看一下execute方法的源碼。我們發(fā)現(xiàn)它接收的入?yún)⑹且粋€Runnable類型。我們按照代碼截圖中的序號依次分析,具體分析內(nèi)容如圖2所示。

圖2
???????? [1]首先,一進方法就先對command進行校驗,如果command為空就直接拋出NPE異常;
???????? [2,3]然后判斷一下線程池中的線程個數(shù)是否小于corePoolSize,如果滿足條件就調(diào)用addWorker(command, true)方法區(qū)執(zhí)行任務。這個方法實際上最終就是開啟了新的線程去執(zhí)行任務。
???????? [4]如果說線程池處于RUNNING狀態(tài),也就是isRunning(c)返回true,那么就將任務添加到阻塞隊列。也就是執(zhí)行workQueue.offer(commmand)。
???????? [5,6]為了確保能夠準確的將任務添加成功,線程池在這里做了二次校驗。這里是因為,如果將任務添加到線程池之后,有可能線程池狀態(tài)已經(jīng)變化了,所以要校驗一下,看看當前的線程池狀態(tài)還是不是RUNNING。
???????? 如果線程池狀態(tài)不是RUNNING了,就把任務從任務隊列中刪除,也就是remove(command),然后就執(zhí)行拒絕策略,也就是調(diào)用reject(command)方法。
???????? [7]如果說線程池狀態(tài)確實是RUNNING,也就是二次校驗通過,那么就判斷一下線程池里是否還有線程,通過WorkerCountOf(recheck) == 0來判斷,如果返回true了,就說明當前線程池中是空的,沒有線程。怎么辦呢?其實很好理解,既然沒有就添加一個就完事兒了,調(diào)用一下addWorker往線程集合里增加一個線程,如圖3所示。

圖3
???????? 看到了吧,實際上所謂的Workers就是一個HashSet,而Worker則本質(zhì)上是一個Runnable,但是它實現(xiàn)了AQS,代碼比較復雜,也很精巧,我們稍安勿躁繼續(xù)慢慢的分析。
???????? [8]在執(zhí)行[4]的時候?qū)θ蝿者M行添加,如果添加失敗就說明任務隊列已經(jīng)滿了,那么只要線程池中的線程數(shù)小于maximumPoolSize就繼續(xù)新開線程來執(zhí)行任務。
???????? 如果線程數(shù)要超過maximumPoolSize了,就需要執(zhí)行拒絕策略了。
??? 到這里我們對提交任務的代碼就分析結(jié)束,這些步驟其實就是本文一開始的圖1,我們看一次加深下印象。

Worker線程獲取任務執(zhí)行流程
通過上面的討論以及我們在之前文章中的鋪墊,大家應該都知道了任務是被線程池中的工作線程(也就是Worker線程)執(zhí)行的了。那我們就來看看,Worker線程是如何獲取任務然后執(zhí)行的。
上面的流程中,我們已經(jīng)知道了線程池是通過執(zhí)行addWorker方法來增加Worker線程的,實際上Worker線程就是在這個階段被啟動的,具體我們來看看代碼,如圖4所示。

圖4
這段代碼是截取的addWorker方法,注意我們用紅框圈住的三個地方,這幾個地方是需要重點關(guān)注的。
首先第一個位置,我們聲明了一個Worker線程,并把它持有的thread成員變量的引用,賦值給final修飾的Thread t臨時變量,然后判斷t是否是alive狀態(tài)。如果是,那么就拋出一個IllegalThreadStateException異常,也就不用啟動了,因為既然已經(jīng)啟動了,就無需在啟動了啊。
第二個位置,將這個新的Worker線程添加到工作線程集合中,通過上文的分析我們知道它是一個HashSet。并設置WorkerAdded狀態(tài)變量為true。
第三個位置,校驗WorkerAdded狀態(tài)變量為true成立,就通過start()方法啟動工作線程,其實最終啟動的是Worker內(nèi)部持有的Thread成員變量。通過Worker的構(gòu)造方法就能知曉其中的奧義,如圖5所示。

圖5
???????? 到這里,我們其實已經(jīng)知道了,最終調(diào)度任務的Worker工作線程,通過構(gòu)造方法我們已經(jīng)知道Worker本質(zhì)上實現(xiàn)了Runnable接口,那么就能夠被Thread啟動,這個想必大家都知道對吧。

圖6
???????? 知道了這個,就好辦了,我們回過頭去研究研究,Worker的run方法。
???????? 為什么要研究run方法呢?兄弟,既然我們都說了Worker是Runnable的實現(xiàn),那最終線程啟動之后,調(diào)度的就是它的run方法邏輯啊,也就是說,Worker肯定是實現(xiàn)了run方法了。代碼是不會說謊的,如圖7所示。

圖7
???????? 怎么樣,Worker線程的確是實現(xiàn)了run方法了,核心邏輯都在runWorker方法里,那我們就繼續(xù)深入。

圖8
???????? 看到這么一大段代碼,想必有的兄弟又開始發(fā)慌了。不慌,這里分享一個讀源碼的小技巧,“抓大放小,分析重點”。我們看源碼重在找到重點,至于其他的細節(jié),慢慢看,甚至于說,你不去深究也無傷大雅,如果鉆牛角尖,非得弄清每行代碼,一方面時間上不現(xiàn)實,一方面肯定會很辛苦,而且得不償失啊。
???????? 從這個“抓大放小,分析重點”的原則出發(fā),我們就關(guān)注紅框圈住的兩行重點代碼:
???????? 第一個位置,我們把Worker的task引用賦值給了Runnable局部變量;
第二個位置要額外關(guān)注,有的兄弟不知道為什么隊列中的任務會被工作線程執(zhí)行,其實就是這句代碼在起作用。通過調(diào)用task = getTask()方法,我截取了getTask方法的核心代碼,我們能夠直觀的看到實際上最終是調(diào)用的workQueue.take()方法取出了隊列中的任務,本質(zhì)上就是生產(chǎn)者-消費者模型。

圖9
???????? 第三個位置,執(zhí)行task的run方法,也就是用戶提交的真正的業(yè)務邏輯。
???????? 你可能想問了,明明這里執(zhí)行的是Runnable的run方法,那工作線程到哪里去了?
???????? 兄弟,這個問題問的有水平!
???????? 我們在上面的分析中,提到了addWorker這個方法,里面就對工作線程進行了start調(diào)用,其實這里就啟動了工作線程,如圖10所示。

圖10
???????? 我們通過一步一步走讀的方式,抽絲剝繭,把Worker線程獲取任務并執(zhí)行的流程全面的展示了出來。通過圖11來展示一下這個過程便于加深理解。
?

圖11
???????? 通過圖11表示的流程,我們主要記住一個結(jié)論:當我們需要向線程池提交任務的時候,通過調(diào)用execute()傳進去的任務(Runnable或者Callable實現(xiàn)),最終會通過Worker的構(gòu)造方法傳遞到Worker內(nèi)部,這樣當start啟動的以后真正執(zhí)行的就是Worker中的Runnable,也就是用戶提交的Runnable。
也許你會說,聽起來有些復雜???不好意思,代碼閱讀的過程本身確實不是一個簡單的事情,但是我們可以把原理講的通俗易懂。因為原理本來就是可以用很簡單很直白的話講清楚的東西。
代碼實現(xiàn)可以很復雜,但是原理越能讓人理解越說明原理是普適的。如果你對代碼不能很好的理解,就把這張圖記住,然后再回去看源碼,相信你會把握住主脈絡。畢竟俗話說,撿了芝麻,丟了西瓜。我們搞技術(shù)的可切忌這樣做,抓大放小才能出奇制勝啊。
Worker線程什么時候退出
?????? 分析完Worker線程的執(zhí)行,我們再趁熱打鐵看看Worker線程是什么時候退出的。
??? 事實上,線程池中的線程銷毀,是要依賴JVM來實現(xiàn)自動回收的。而線程池自身,會根據(jù)當前線程池的狀態(tài)來維持一定數(shù)量的線程引用,防止該部分的線程被JVM回收掉。
??? 如果線程池決定了哪些線程要被回收,那么就將他們的引用消除即可。
??? 我們在之前的學習中知道,Worker線程一旦被創(chuàng)建好了,就會持續(xù)的輪詢獲取任務去執(zhí)行。
??? 對于核心線程來說,他們可以無限制的等待著任務被獲取并執(zhí)行,而非核心的線程則是在有限的時間內(nèi)獲取任務,一旦Worker無法獲取到任務,也就是要獲取的任務為空,循環(huán)就會結(jié)束,Worker自己就會主動的去除掉在線程池中的應用,進而被回收掉并退出。
??? 從代碼角度看一下這個過程是在什么時候發(fā)生的吧,如圖12所示。

圖12
??? 我們還是回到runWorker方法,發(fā)現(xiàn)在try塊中有個while循環(huán),循環(huán)停止的條件就是要獲取的任務為空以及通過getTask獲取不到任務了,最終會進到finally塊執(zhí)行收尾邏輯,如圖13所示。

圖13
??? 首先[1]部分的代碼是說,在執(zhí)行線程退出之前,線程池會先統(tǒng)計池子里完成任務的數(shù)量,然后通過workers.remove(w)把Worker移除掉。要注意的是在統(tǒng)計之前加了全局鎖,保證統(tǒng)計的準確性。
??? [2]部分的代碼是說,如果當前線程池狀態(tài)是SHUTDOWN狀態(tài)并且工作隊列已經(jīng)為空,或者當前線程池已經(jīng)是STOP狀態(tài),或者說當前線程池中沒有活動的線程,則嘗試對線程池狀態(tài)設置為TERMINATED。
??? [3]部分是說,最后還是得判斷一下線程池里面的實際線程數(shù)是否小于核心的線程個數(shù),如果是的話,就得增加線程。這里的目的是保證線程池中的核心線程數(shù)量不變。
??? 怎么樣,是不是覺得線程池沒有那么可怕了?我們繼續(xù)趁熱打鐵,通過一張圖來總結(jié)一下Worker線程退出的邏輯,如圖14所示。

圖14
??? 面試中如果遇到這個問題,那么就把這張圖的過程說出來就可以了,你可以說,我是在閱讀并理解了源碼的基礎(chǔ)上總結(jié)出來的。相信你一定能夠得到面試官的青睞。
??? 到這里,本節(jié)的文章又告一段落,感謝你一直看到現(xiàn)在。
??? 確實這篇文章相對比較硬核,我們畢竟是需要通過閱讀源碼的方式去加深理解。通過對本節(jié)的學習,相信你對線程池提交任務的過程、Worker線程獲取任務并執(zhí)行的過程以及Worker線程退出過程都有了源碼級的深入了解。同時筆者閱讀的源碼“抓大放小,分析重點”的方法,相信你也get到精髓了,希望你能夠利用這種思路,征服源碼,收獲更多。
??? 在之后的章節(jié)中,我們將一同從實戰(zhàn)角度出發(fā),研究如何自定義線程池滿足不同的業(yè)務場景,敬請期待,
