一、概述
筆者在網上看了好多的關于線程池原理、源碼分析相關的文章,但是說實話,沒有一篇讓我覺得讀完之后豁然開朗,完完全全的明白線程池,要么寫的太簡單,只寫了一點皮毛,要么就是是晦澀難懂,看完之后幾乎都是一知半解。我想要么是筆者智商捉急,要么就是那些寫博客的人以為我很懂所以就大概講了講,再或者是作者壓根就沒認真去講述線程池。當然多線程以及并發這一塊的知識點本身就比較晦澀難懂,但是也不至于找不到一篇文章解惑。于是筆者就下定決心,自己去網上收集資料,自己去買書看那些大神的講解,然后收集百家之所長,整理一篇不僅適合初學者學習,還適合讓老鳥查漏補缺的史上最通俗易懂的線程池知識相關的文章。
寫在最前
在寫文章之前我的大綱里面是有一節實戰的,但是最后還是選擇了刪掉,不是筆者寫不下去了,而是實在是文字有點太多了,我怕讀者看到文章這么長望而生畏,所以刪掉。又怕自己的文筆太差而讓讀者產生某些誤解,所以很多東西都在重復的去說,以至于隨便寫了寫就已經小一萬字了,這里和各位讀者說聲抱歉。筆者寫文章的理念就是精簡、明確、表達清晰,但是這部分內容實在不太好分開來寫,實際上也沒有特別多的內容,而且每一小部分我都有相應的總結,如果認真看,看懂應該沒問題。最后,還是希望讀者能夠耐心看完,能夠有所收獲。
我知道還有有很多人沒有那么多耐心,看不到最后,那么你們就先看總結吧,希望對你們能有一點幫助。
本文首發于心安-XinAnzzZ 的個人博客,轉載請注明出處~
二、線程池簡介
1) 線程池是什么?
線程池就是指管理一組同構工作線程的資源池。每次應用程序需要創建一個線程來執行任務的時候不會直接創建線程,而是從線程池中取出線程,線程結束之后也不會直接銷毀線程,而是放回線程留給其他任務使用。通過重用現有的線程而不是創建新線程,這樣可以避免反復的創建和銷毀線程,從而達到節省系統資源的目的。
2) 線程池的作用
我們通過一個對比來看一下線程池的作用。假如應用程序需要同時做三件事:讀取磁盤文件、分析文件內容、寫入數據庫。
-
不使用線程池
應用程序要手動的繼承
Thread
類或者實現Runnable
接口來創建三個線程,分別用于讀文件、分析內容。寫庫。當事情完成的時候,線程結束被銷毀。 -
使用線程池
應用程序在啟動的時候創建線程池,然后這三個任務來了之后,新建三個線程放入到線程池,分別用于執行任務,任務完成不會銷毀線程,而是繼續放在池中,當其他任務在需要新線程來執行任務的時候可以復用這些線程。
所以說,合理的使用線程池將會為我們帶來以下好處:
- 降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要等到線程創建就可以立即執行。
- 提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。
但是,要做到合理利用線程池,必須對其實現原理了如指掌。
3) 線程池是如何實現的
Java 中萬物皆對象,線程池也是一個對象,在 Java 中使用java.util.concurrent.ThreadPoolExecutor
這個類來實現線程池,這是線程池框架的最核心的類,也是后面我們分析線程池源碼的核心對象,我們提前簡單認識一下。既然是池,那就意味著它是一個容器,那么它是一個什么樣的容器呢?閱讀ThreadPoolExecutor
類的源碼可以發現它內部有一個類型為HashSet<Worker>
的workers
字段,這個就是用來保存線程的容器。可以看見這個容器裝的元素類型為Worker
類型,這個是ThreadPoolExecutor
的一個內部類,它實現了Runnable
接口,也就是說它就是一個線程類。那么我們大體上就應該明白,每次需要新線程的時候就會創建一個Worker
對象,然后加入到這個Set
中。下面我說一下線程的工作流程再配以故事和圖解:
4) 線程池是如何工作的
-
線程池的組成部分(最少具有以下四個部分)
線程池管理器:用于創建和并管理線程
工作線程:線程池中的線程
任務接口:每個任務必須實現的接口,用于工作線程調度執行
任務隊列:用于存放待處理的任務,或者稱為工作隊列。
-
再說幾個常見的概念,如果覺得概念性的東西不清楚,可以先看下面的工作流程,結合實際來理解這些概念。
- corePoolSize:核心線程數(有些資料稱為基本池大小,只是稱呼問題而已),這個指的是在線程池創建的時候指定的線程數量。當提交一個任務到線程池的時候,線程池會創建一個線程來執行任務,即使其他空閑的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大于基本大小就不會立即創建新的線程。
- maximumPoolSize:最大池大小,這個就是線程池最大的線程數量。它和基本大小的區別簡單來說就是,正常情況下,池大小等于核心池數量,但是任務特別多,線程池特別忙的時候,就再多創建幾個線程來"幫忙",但是無論如何都不能大于最大池大小,有些資料把這些"幫忙"的線程稱之為擴展線程池。當線程池空閑的時候會銷毀掉部分"幫忙"的線程使池大小恢復到核心池大小,我理解的意思就是卸磨殺驢233333。這里再多說一句,只有一個池,核心池和擴展池只是邏輯上的概念,實際上它們都在一個池中,也就是上面說的那個
HashSet
。 - ThreadFactory:線程工廠,可以通過線程工廠來給每個創建的線程設置有意義的名字。
- RejectedExecutionHandle:飽和策略,當線程池滿了,并且任務隊列滿了,對于新提交的任務的處理策略,默認情況下是 AbortPolicy,表示無法處理新任務時拋異常。簡單理解就是,活太多,老子要罷工了,那么罷工的方式是啥呢,默認情況拋異常,當然也提供了其他的策略,這個后面我們再詳細了解。
-
線程池的工作流程
當線程池創建之后會有任務提交給線程池來處理,那么線程池是如何處理的呢?我們看一下具體流程:
- 提交一個新任務,判斷池中線程數量是否小于線程池的核心池大小(corePoolSize),如果小于,就創建一個新的線程來執行這個任務。否則,也就是線程數量已經大于等于核心池大小,那么進入下一步。
- 判斷任務隊列是否是否已滿,如果任務隊列沒滿,就存放在任務隊列中。等著工作線程一個一個的從任務隊列中取出任務來執行。如果隊列已滿,進入下一步。
- 判斷線程池大小是否達到最大池大小(maximumPoolSize),如果未達到,則創建新的線程來處理任務。否則,也就是線程池已達到最大大小,則采取飽和策略。
- 當任務被執行完,線程池比較空閑的時候就會把大于核心線程池數量的那部分線程池(擴展線程池)中的線程銷毀掉。
所以提交任務的順序是:核心線程池—任務隊列—擴展線程池。請看以下流程圖:
image
上面的流程其實已經很簡單明了了,但是為了方便讀者理解,筆者再通過一個現實場景來模擬線程池的運行過程。 -
外賣員送外賣
小明家樓下有一家炸雞店(讀者可能會疑惑為什么是炸雞店呢?難不成線程池和炸雞之間有著某種不可告人的秘密?別想太多,單純是因為筆者愛吃炸雞),每天都有很多外賣單子需要外賣小哥來送。那么這里面"外賣"就是“任務”,外賣小哥就是"線程",外賣小哥送外賣就是線程執行任務,外賣送不完就在店里面排著隊等著外賣小哥來送,這個外賣排著的隊伍就叫"任務隊列"。
- 最開始情況下,生意不是很好,偶爾來一個外賣就叫一個外賣小哥來送外賣(這就類比應用程序有一個任務,就起一個線程來執行任務)。外賣送完了,外賣小哥就下班了(線程銷毀了)。
- 后面生意慢慢好起來了,老板發現,每次外面小哥送完就下班,再來單子又要雇一個外賣小哥,麻煩的要死,還花很多冤枉錢,于是老板就雇了一個外賣團隊(線程池),然后老板為了節省錢,同時為了應對偶爾的外賣高峰期,決定了團隊就 10 個人(corePoolSize),但是高峰期的時候允許請5個臨時工,共 15 人(maximumPoolSize)。
- 這時候外賣的運營就是類似上面的流程了,來一個外賣單子,老板就看一下,外賣團隊有閑人嗎?有,那就去送外賣。如果這 10 個人去送外面了,那就把單子先放在店里排隊排起來(這個就是任務隊列),等著某個小哥送完了手里的單子,就從隊列中取外賣單繼續送。
- 到了中午,外賣單子越來越多,外賣單子的隊伍也排的越來越長,老板覺得不能這樣,這樣用戶等太久了會差評的,就規定,最多 50 個外賣單子排隊,再多了就請臨時工,但是上面規定了,最多再請 5 個臨時工。
- 于是當再來單子的時候就請臨時工來送,但是單子實在太多,就算 15 個人也送不過來,外賣小哥累成狗,決定要罷工,再來單子老子不干了。再來單子的時候,隊伍也滿了,外賣員人數也滿了,沒辦法,老板只能打電話告訴買家,抱歉啊,暫時不接單了,麻煩申請退款一下吧(拒絕策略)。
- 高峰期過去了,外賣單子都送完了,好多外賣員也歇著了,老板說這也不能白養著這群人啊,把臨時工辭退了吧。嗯,沒錯這就是上面我說的,卸磨殺驢。
三、線程池源碼分析
通過上面的講解,相信讀者已經能夠明白線程池是什么、能做什么以及如何做的。那么下面就結合源碼來剖析線程池的工作原理。以下所有源碼均來自java.util.concurrent
包下,這個包通常被簡稱為J.U.C
。本文使用源碼版本為Java8.
1) Execuor 框架
-
Executor
public interface Executor { void execute(Runnable command); }
頂級接口,雖然只有一個簡單的方法,但是它是 Executor 框架的基礎,它將任務的提交和執行解耦。這個
Execute
方法就是用來提交任務,線程池需要重寫這個方法來實現提交任務的邏輯。 -
ExecutorService
它是對
Executor
的擴展,增加了一些管理線程生命周期的方法和任務生命周期的方法。 -
AbstractExecutorService
它是對
ExecutorService
的抽象實現,不是本文分析的重點。 -
ThreadPoolExecutor
Java 線程池的核心實現,本文分析的重點。
2) ThreadPoolExecutor源碼分析
-
核心成員變量解讀
// 以下所有中文注釋為筆者添加,英文注釋為作者添加 // ctl 打包了 runState 和 workerCount private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // Integer.SIZE = 32 - 3 = 29,29 個比特位 private static final int COUNT_BITS = Integer.SIZE - 3; // 池最大線程數量,大概 5 億 private static final int CAPACITY = (1 << COUNT_BITS) - 1; // runState is stored in the high-order bits // 線程池運行狀態存儲在高三位中,作者注釋真的很清晰 private static final int RUNNING = -1 << COUNT_BITS; private static final int SHUTDOWN = 0 << COUNT_BITS; private static final int STOP = 1 << COUNT_BITS; private static final int TIDYING = 2 << COUNT_BITS; private static final int TERMINATED = 3 << COUNT_BITS; // Packing and unpacking ctl // 下面三個方法是用來打包和拆包 ctl // 拆包 runState private static int runStateOf(int c) { return c & ~CAPACITY; } // 拆包 workerCount private static int workerCountOf(int c) { return c & CAPACITY; } // 打包 runState 和 workerCount private static int ctlOf(int rs, int wc) { return rs | wc; }
關于這些成員變量的含義,在ctl
變量的注釋中作者已經進行了詳細的解釋說明,如果你懂這些成員的意義并且你的英文能力不錯的話,那么這個注釋你讀完一遍你就會發現,哇,作者寫的真棒,但是如果你不懂或者你英文很爛,你就會發自肺腑的說一句,這什么破玩意。。
ok,不扯淡,筆者來解讀一下作者的注釋。首先,這個ctl
它"打包"(原文是"packing")了用來表示線程池工作線程數量和線程池運行狀態的兩個值。如何打包的呢?一個int
型數據有 32 位,ctl
的高 3 位表示線程池狀態,低 29 位表示線程數量29 位大概可以表示 5 億個線程。為什么是 3,而不是別的值呢?因為線程池狀態有 5 種,分別為RUNNING
、SHUTDOWN
、STOP
、TIDYING
以及TERMINATED
,如果小于 3 位則不夠表示 5 種狀態,大于 3 位又浪費。到這里前面的成員變量的意義沒什么問題了。為什么用一個 int 表示兩個狀態呢?作者的解釋是更快更簡單,我覺得不僅如此,還更裝逼,嗯沒錯。熟悉讀寫鎖ReadWriteLock
的大神肯定清楚,讀寫鎖也是用一個int 來分別表示讀寫鎖狀態。
再看一下下面三個方法,作者的注釋翻譯過來是:打包和拆包ctl
,也就是我想獲取runState
咋獲取,調用runStateOf
,然后傳入當前的ctl
就可以了。再簡單理解就是runState
和workerCount
這倆玩意的getter
和setter
。下面是線程池不同狀態對應的數值及其意義:
runState | 對應的高三位的數值 | 原文 | 翻譯 |
---|---|---|---|
RUNNING | 111 | Accept new tasks and process queued tasks | 接受新任務并且處理隊列中的任務 |
SHUTDOWN | 000 | Don't accept new tasks, but process queued tasks | 不接受新任務,但是處理隊列中的任務 |
STOP | 001 | Don't accept new tasks, don't process queued tasks, and interrupt in-progress tasks | 不接受新任務,不處理隊列中的任務,并且會中斷正在執行的任務 |
TIDYING | 010 | All tasks have terminated, workerCount is zero, the thread transitioning to state TIDYING will run the terminated() hook method | 所有任務都已經結束,workerCount = 0,線程轉換到 TIDYING 狀態,將會執行 terminated()鉤子函數 |
TERMINATED | 011 | terminated() has completed | 鉤子函數 terminated()執行完畢 |
所以,總結一下,上面的成員變量和三個輔助函數就是為了表示線程數量和線程池狀態。下面看一下構造方法:
-
其他成員
// 核心池大小 private volatile int corePoolSize; // 最大池大小 private volatile int maximumPoolSize; // 阻塞隊列,存放任務的隊列 private final BlockingQueue<Runnable> workQueue; // 存放 worker 線程的集合 private final HashSet<Worker> workers = new HashSet<Worker>(); // 最大池大小,區分與 maximumPoolSize // largestPoolSize 只是記錄池中的線程數量曾經達到的最大值 // 而 maximumPoolSize 是創建線程池時候指定的對于池大小的限制 private int largestPoolSize; // 線程空閑的時間,上面說的卸磨殺驢等待的時間 private volatile long keepAliveTime; // 完成任務數量 private long completedTaskCount; // 線程工廠 private volatile ThreadFactory threadFactory; // 拒絕策略,默認提供了四種拒絕策略,都不太好用,不詳細說了 private volatile RejectedExecutionHandler handler; // 默認的拒絕策略,拋出運行時異常,實際生產還是要自己實現拒絕策略 private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
-
構造函數
源碼有四個構造函數,但是三個都是重載,看下面這一個就可以了:
public ThreadPoolExecutor(int corePoolSize, // 核心池大小 int maximumPoolSize, // 池最大線程數 long keepAliveTime, // 存活時間 TimeUnit unit, // 時間單位 // 阻塞隊列,也就是任務隊列,稱為工作隊列也行,whatever BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, // 線程工廠 RejectedExecutionHandler handler // 拒絕策略) { // 篇幅有限,省略若干代碼 }
簡單解釋一下存活時間,就是前面所說的,線程池空閑之后會把超過核心線程池部分的線程干掉,但是不是立馬干掉,還是有個緩沖期的,這個就是這個緩沖期,配合下面的時間單位使用。構造器內部也沒有特別的邏輯,相信聰明的讀者看一眼源碼就懂。
-
其他常見方法
/** * 關閉線程池,調用后已提交的任務會繼續執行,但是不再接受新任務 * 也就是說,該方法被調用后線程池狀態變為 SHUTDOWN 狀態 * 如果該方法被調用多次不會產生副作用 */ public void shutdown() { /* 省略方法體 */ } /** * 嘗試停止所以正在執行的任務,并且將任務全部移除 * 該方法被調用之后線程池狀態變為 STOP */ public void shutdownNow() { /* 省略方法體 */ }
3) 核心方法源碼分析
根據前面的流程分析,線程池核心就是提交任務,然后添加核心線程,添加到任務隊列等等,一切的一切都始于提交任務,所以我們最先要分析的就是提交任務的方法execute(Runnable command)
,但是大概看一眼源碼可以發現,這個方法本身只有一些邏輯判斷,然后根據不同的邏輯去調用其他邏輯方法,而最多調用的是添加worker
的方法addWorker(Runnable firstTask, boolean core)
。
所以想要理解線程池原理就要看懂execute
方法,想看懂execute
方法就要先看懂addWorker
方法。下面我們就來分析一下addWorker
方法,然后再分析execute
方法。
-
addWorker()源碼分析
源碼中作者為這個方法添加了很多的注釋,這里筆者通過翻譯軟件以及結合源碼說一下自己的理解:
首先說一下方法的參數,第一個參數是
firstTask
,這個比較簡單,就是新創建的worker
,前文已經說過,他就是線程對象,那么它執行的第一個任務,通過這個參數來指定,可以指定為null
。簡單來說,當workerCount
小于corePoolSize
或者隊列已滿需要創建擴展線程時,都將新提交的任務直接指定給新創建的線程,而不是讓這個任務去排隊。第二個參數是
Boolean core
,也就是指定要創建的新的worker
是不是核心線程。這個很簡單,這個參數在源碼中就用到一次。下面源碼里面有介紹。然后方法的作用就是添加一個
worker
,作者在注釋中寫道,根據當前的池狀態以及池大小邊界(核心池大小或者最大池大小)來檢查是否可以添加新的worker
,如果可以就添加并且修改workerCount
,同時如果可能的話,將firstTask
作為這個worker
的第一個任務來執行。如果因為池狀態或者無法創建線程等原因創建失敗,返回 false。下面結合源碼分析:private boolean addWorker(Runnable firstTask, boolean core) { retry: for (; ; ) { int c = ctl.get(); // 獲取到池運行狀態 int rs = runStateOf(c); // Check if queue empty only if necessary. // 如果運行狀態大于等于 SHUTDOWN,也就是說池處于非 RUNNING 狀態 // 并且 !(狀態等于 SHUTDOWN的同時 fistTask 為空且隊列不為空) // 符合以上條件,返回添加失敗 if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) return false; for (; ; ) { // 獲取工作線程的數量 int wc = workerCountOf(c); // 如果數量大于等于池最大線程數量 // 或者說,如果大于當前池限制的池大小(如果是核心池就是核心池大小,否則,就是最大池大小) // 那么也返回 false 添加失敗 // 也就是說你在調用這個方法的時候就需要判斷當前池大小 // 如果當前池大小小于核心池大小,那么你添加的就是核心線程,傳遞 true,否則 false // 這個最大數量和 maximumPoolSize 不一樣,具體請看前面的源碼解讀 if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; // CAS 嘗試增加 workerCount,注意,是先增加數量,而實際上還沒有增加 worker // 這個也和作者注釋中描述的一樣,先檢查能否添加,然后增加 worker count // 并且如果可能,新建 worker 并且啟動它 if (compareAndIncrementWorkerCount(c)) // 如果增加成功,那么就跳出 retry到第 41 行代碼 break retry; // 如果沒增加成功,說明 ctl 被其他線程更改了,那就重試 c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } // 上面增加 worker count成功,就走到了這兒,開始嘗試創建 worker // 新建兩個狀態標記,分別表示 worker 是否已添加和已啟動 boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { // 上鎖,因為 HashSet 不是線程安全的 //如果不了解 ReentrantLock 可以簡單認為這個try catch 被 synchronized塊 包裹 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // Recheck while holding lock. // Back out on ThreadFactory failure or if // shut down before lock acquired. int rs = runStateOf(ctl.get()); // 再次確認線程池狀態 小于 SHUTDOWN 說明是 RUNNING 狀態 // 或者已經是 SHUTDOWN 狀態同時 firstTask 為 null if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); // 將新創建的線程添加到 workers 去 workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } if (workerAdded) { t.start(); workerStarted = true; } } } finally { if (!workerStarted) // 如果因為某種原因啟動失敗,就調用這個方法 // 這個方法主要是將新添加的 worker 從池中移除并且將 workerCount 減一 addWorkerFailed(w); } return workerStarted; }
上面的源碼看起來很復雜,但是其實仔細看看很多都是循環操作、狀態判斷操作、加鎖解鎖操作。實際上核心操作總結起來就三步:
- 通過判斷當前池狀態以及傳遞的參數狀態,來決定是否添加 worker,如果此時的狀態不能夠添加,返回
false
。 - 可以添加就嘗試使用
CAS
來增加workerCount
,如果因為別的線程更改了ctl
變量而導致增加失敗,就回到第一步重試。如果增加成功,進入下一步。 - 數量增加成功,創建新的
worker
。為了保證線程安全,進行了加鎖操作,可以忽略。然后繼續各種判斷,池狀態、worker
狀態等等,如果都沒問題,把worker
添加到池中,并且嘗試啟動。這里面如果出現問題,那就把worker
從池中移除,并且將workerCount
減一,返回false
。
- 通過判斷當前池狀態以及傳遞的參數狀態,來決定是否添加 worker,如果此時的狀態不能夠添加,返回
-
execute()源碼分析
上面的邏輯看明白了,這個方法也就沒太多難點,直接看源碼以及注釋:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); // 如果 當前的線程數量 < 核心池大小 就添加一個 Worker if (workerCountOf(c) < corePoolSize) { // 把正在提交的任務作為新建的 worker 的第一個任務,并且標識是核心線程 if (addWorker(command, true)) // 添加成功就結束了 return; // 如果沒添加成功,重新獲取ctl c = ctl.get(); } // 走到這兒說明核心池已滿,按照最上面的流程分析, // 此時可能添加任務到任務隊列,可能新建 “擴展線程池”的 worker 來處理 // 也可能采取拒絕策略 // 這里判斷如果還是運行狀態,并且成功添加到工作隊列 if (isRunning(c) && workQueue.offer(command)) { // 再次檢查狀態(筆者內心:多線程就是蛋疼,一直檢查,就怕別人改了。。。) int recheck = ctl.get(); // 如果不是運行狀態,那么就把任務移除 并且拒絕掉這個任務 if (!isRunning(recheck) && remove(command)) reject(command); // 代碼走到這里就說明可能是運行狀態或者移除任務失敗,再次檢查workerCount else if (workerCountOf(recheck) == 0) // 上面源碼已經分析過了 addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }
到這里,該創建
worker
也創建了,該提交任務到隊列也提交了,外賣員有了,外賣也"提交"了,下一步就應該是如何送外賣了,ok,我們來看看這個worker
如何工作的。
4) Worker:工人是如何工作的。
前面已經簡單的介紹了一下,Worker
是Runnable
的子類,也就是線程類。那么我們先看看它的結構。
-
類結構
image首先,
Worker
實現了兩個接口,一個是AQS
同步器接口,一個是Runnable
,AQS
是為了實現自己的同步策略,這里思考一下,為什么不直接用ReentrantLock
呢?答案是線程執行任務時是不允許其它鎖重入進來的,而前者可重入,所以不可用。同步相關的方法不是我們討論的核心,所以我們不用考慮,所以,就主要看
run
方法就行了。源碼里面run
方法調用了runWorker
方法,下面分析一下這個方法。 -
runWorker 方法源碼分析
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; // 這里的操作是為什么呢?其實看一下`Worker`的構造方法可以發現 // 構造的時候有一句"setState(-1);" 這個是 AQS,我這里不具體分析,只解釋作用 // 這句話后面,作者注釋 inhibit interrupts until runWorker 意思是 禁止中斷線程,直到 runWorker // 這里 unlock 后面注釋 allow interrupts 就是允許中斷 // ok,這應該就明白了,創建 worker 的時候設置禁止中斷,runWorker 之后設置允許中斷 w.unlock(); // allow interrupts boolean completedAbruptly = true; try { // 這段代碼很重要,如果 task 不為空或者 getTask 不為空!!getTask 就是從任務隊列取任務 // 這就說明 worker 一直在循環從任務隊列取任務來執行 while (task != null || (task = getTask()) != null) { w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt // 這里不解釋了,直接翻譯作者的注釋 // 如果線程池已經停止,確保線程已經被中斷,如果沒有停止,確保線程不被中斷. // 在第二種情況下需要重新檢查來處理 因為調用了 shutdownNow 方法而產生的競爭 if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { // 在執行任務之前要執行的操作,默認情況下是空實現,什么都不做,如果子類有特殊的需求可以重寫 beforeExecute(wt, task); Throwable thrown = null; try { // 這里調用的是 run!!不是 start 方法!!!我想讀者在最開始學習創建線程的時候,應該都看過線程的 run 方法和 start 方法的區別 // run 方法只是一個普通的方法,而 start 才是啟動線程的方法 // 所以這里需要注意,這里是 worker 來執行任務,而不是讓 worker 來啟動新的線程來執行任務 // 所以調用的是 run 而不是 start,如果調用 start,那豈不是等于說,線程池里面每個任務都會新創建一個線程? task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { // 默認空實現,什么都不做 afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { // 到這里說明任務全部完成,結束線程 processWorkerExit(w, completedAbruptly); } }
總結起來三個步驟:
- 循環取任務來消費,調用
getTask
方法取任務,調用run
方法執行任務。 - 如果線程池正在停止,則中斷線程。
- 取到的任務為
null
,跳出循環,移除線程(processWorkerExit
方法會執行相應的邏輯,具體不分析)。
- 循環取任務來消費,調用
5) 總結
在第二部分線程池簡介的時候我們已經分析詳細的描述了線程池的工作流程,但是那只是理論,這一節我們通過代碼具體的了解到了線程池的運行原理。總結起來主要三個東西:
-
提交任務
execute
方法提交任務,然后根據池狀態來判斷是否接受任務,不接受采用拒絕策略;能夠接受任務,判斷是需要創建新的worker
還是直接加入到任務隊列; -
添加 worker
通過
retry
來不斷地嘗試,判斷能否添加,不能返回false
;能的話就嘗試增加workerCount
;然后創建worker
,然后啟動。 -
執行任務
線程循環從任務隊列取出任務來執行,直到隊列為空。
四、總結
-
思維導圖:
思維導圖 -
線程池的作用:
- 降低資源消耗
- 提高響應速度
- 提高線程的可管理性
-
ThreadPoolExecutor 重要成員:
- 使用一個
AtomicInteger
變量ctl
來表示workerCount
(工作線程數量)和runState
(線程池運行狀態)。 - corePoolSize:核心線程數量。
- maximumPoolSize:線程池最多線程數量。
- workers:線程集合,存放工作線程。
- workQueue:工作隊列,或稱為任務隊列。
- handler:類型為
RejectedExecutionHandler
,拒絕策略。 - threadFactory:線程工廠。
- 線程池五種狀態:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。
- 使用一個
-
線程池的整體運行流程:
image- 創建線程池,并且通過
execute
方法往線程池中提交多個任務。 - 此時線程池線程數量比較少,線程池不斷創建核心線程來處理任務,直到線程數量等于
corePoolSize
。 - 當任務很多,所有核心線程都在處理任務時,新提交的任務沒有線程處理,則放入到工作隊列等待工作線程來處理。
- 工作線程處理完成一個任務之后去工作隊列取任務來執行,直到隊列為空,結束線程。
- 如果一直往工作隊列中提交任務導致工作隊列滿了,就繼續創建線程來處理任務,直到線程數量等于
maximumPoolSize
。 - 線程數量已經達到最大限制并且隊列滿了,就會采取拒絕策略,默認拋異常。
- 創建線程池,并且通過
五、參考
-
《Java并發編程的藝術》— 方騰飛 魏鵬 程曉明 著
-
《Java 并發編程實戰》
-
JAVA線程池代碼淺析
-
理解java線程池