ThreadPoolExecutor 可能在別的地方已經看過好多了,那我就儘量講點兒不一樣的知識出來
爲什麼要用線程池
你有沒有這樣的疑惑,爲什麼要用線程池呢?可能你會說,我可以複用已經創建的線程呀;線程是個重量級對象,爲了避免頻繁創建和銷燬,使用線程池來管理最好了
沒毛病,各位都很懂哈~
不過使用線程池還有一個重要的點:可以控制併發的數量.如果併發數量太多了,導致消耗的資源增多,直接把服務器給搞趴下了,肯定也是不行的
咱們再看看 ThreadPoolExecutor ,把這三個單詞分開看, Thread 線程
, Pool 池
, Executor
執行者.如果連起來的話,是線程池執行者
所以呢, ThreadPoolExecutor 它強調的是 Executor
,而不是一般意義上的池化資源
繞不過去的幾個參數
提到 ThreadPoolExecutor 那麼你的小腦袋肯定會想到那麼幾個參數,咱們來瞅瞅源碼(我就直接放有 7 個參數的那個方法了):
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
咱們分別來看:
- corePoolSize :
核心線程數,在線程池中有兩種線程,核心線程和非核心線程.在線程池中的核心線程,就算是它什麼都不做,也會一直在線程池中,除非設置了allowCoreThreadTimeOut
參數 - maximumPoolSize:
線程池能夠創建的最大線程數.這個值 = 核心線程數 + 非核心線程數 - keepAliveTime & unit :
線程池是可以撤銷線程的,那麼什麼時候撤銷呢?一個線程如果在一段時間內,都沒有執行任務,那說明這個線程很閒啊,那是不是就可以把它撤銷掉了?
所以呢,如果一個線程不是核心線程,而且在 keepAliveTime & unit 這段時間內,還沒有幹活,那麼很抱歉,只能請你走人了
核心線程就算是很閒,也不會將它從線程池中清除,沒辦法誰讓它是core
線程呢~ - workQueue :
工作隊列,這個隊列維護的是等待執行的 Runnable 任務對象
常用的幾個隊列: LinkedBlockingQueue , ArrayBlockingQueue , SynchronousQueue , DelayQueue
大廠的編碼規範,相信各位都知道,並不建議使用 Executors ,最重要的一個原因就是: Executors 提供的很多方法默認使用的都是無界的 LinkedBlockingQueue ,在高負載情況下,無界隊列很容易就導致 OOM ,而 OOM 會讓所有請求都無法處理,所以在使用時,強烈建議使用有界隊列,因爲如果你使用的是有界隊列的話,當線程數量太多時,它會走拒絕策略 - threadFactory :
創建線程的工廠,用來批量創建線程的.如果不指定的話,就會創建一個默認的線程工廠 - handler :
拒絕處理策略.在 workQueue 那裏說了,如果使用的是有界隊列,那麼當線程數量大於最大線程數的時候,拒絕處理策略就起到作用了
常用的有四種處理策略:- AbortPolicy :默認的拒絕策略,會丟棄任務並拋出 RejectedExecutionException 異常
- CallerRunsPolicy :提交任務的線程,自己去執行這個任務
- DiscardOldestPolicy :直接丟棄新來的任務,也沒有任何異常拋出
- DiscardOldestPolicy :丟棄最老的任務,然後將新任務加入到工作隊列中
默認拒絕策略是 AbortPolicy ,會 throw RejectedExecutionException 異常,但是這是一個運行時異常,對於運行時異常編譯器不會強制 catch 它,所以就會比較容易忽略掉錯誤.
所以,如果線程池處理的任務非常重要,儘量自定義自己的拒絕策略
線程池的幾個狀態
這篇文章開始我就說了,希望能寫出一點兒不一樣的東西,那咱們就從源碼擼一擼
擼啥呢,源碼那麼多,總不能毫無目的的擼吧?
咱們來吧線程池的 5 種狀態來擼一擼
在源碼中,我們能夠很明顯看到定義的 5 種狀態:
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;
同時,使用 AtomicInteger 修飾的變量 ctl 來控制線程池的狀態,而 ctl 保存了 2 個變量:一個是 rs 即 runState ,線程池的運行狀態;一個是 wc 即 workerCount ,線程池中活動線程的數量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int ctlOf(int rs, int wc) { return rs | wc; }
- 線程池創建之後就處於 RUNNING 狀態
- 調用 shutdown() 方法之後處於 SHUTDOWN 狀態,此時線程池不再接受新的任務,清除一些空閒 worker ,等待阻塞隊列的任務完成
- 調用 shutdownNow() 方法後處於 STOP 狀態,此時線程池不再接受新的任務,中斷所有的線程,阻塞隊列中沒有被執行的任務也會被全部丟棄
- 當線程池中執行的任務爲空時,也就是此時 ctl 的值爲 0 時,線程池會變爲 TIDYING 狀態,接下來會執行 terminated() 方法
- 執行完 terminated() 方法之後,線程池的狀態就由 TIDYING 轉到 TERMINATED 狀態
最後上張圖總結一下:
線程池是如何處理任務的
線程池處理任務的核心方法是 execute ,大概思路就是:
- 如果 command 爲 null ,沒啥說的,直接拋出異常就完事兒了
- 如果當前線程數小於 corePoolSize ,會新建一個核心線程執行任務
- 如果當前線程數不小於 corePoolSize ,就會將任務放到隊列中等待,如果任務排隊成功,仍然需要檢查是否應該添加線程,所以需要重新檢查狀態,並且在必要時回滾排隊;如果線程池處於 running 狀態,但是此時沒有線程,就會創建線程
- 如果沒有辦法給任務排隊,說明這個時候,緩存隊列滿了,而且線程數達到了 maximumPoolSize 或者是線程池關閉了,系統沒辦法再響應新的請求,此時會執行拒絕策略
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 當前線程數小於 corePoolSize 時,調用 addWorker 創建核心線程來執行任務
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 當前線程數不小於 corePoolSize ,就將任務添加到 workQueue 中
if (isRunning(c) && workQueue.offer(command)) {
// 獲取到當前線程的狀態,賦值給 recheck ,是爲了重新檢查狀態
int recheck = ctl.get();
// 如果 isRunning 返回 false ,那就 remove 掉這個任務,然後執行拒絕策略,也就是回滾重新排隊
if (! isRunning(recheck) && remove(command))
reject(command);
// 線程池處於 running 狀態,但是沒有線程,那就創建線程執行任務
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果放入 workQueue 失敗,嘗試通過創建非核心線程來執行任務
// 如果還是失敗,說明線程池已經關閉或者已經飽和,會拒絕執行該任務
else if (!addWorker(command, false))
reject(command);
}
在上面源碼中,判斷了兩次線程池的狀態,爲什麼要這麼做呢?
這是因爲在多線程環境下,線程池的狀態是時刻發生變化的,可能剛獲取線程池狀態之後,這個狀態就立刻發生了改變.如果沒有二次檢查的話,線程池處於非 RUNNING 狀態時, command 就永遠不會執行
來張圖,總結一下上面說的:
這篇文章寫到這裏就沒有啦~
希望你能從中得到一些收穫
感謝你的閱讀哇