[Java 併發]詳解 ThreadPoolExecutor

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 就永遠不會執行
來張圖,總結一下上面說的:
在這裏插入圖片描述
這篇文章寫到這裏就沒有啦~
希望你能從中得到一些收穫
感謝你的閱讀哇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章