Java原始碼解析 - ThreadPoolExecutor 執行緒池

JavaEdge發表於2019-01-19

聯絡我

image
圖片標題
1.Q群【Java開發技術交流】:jq.qq.com/?_wv=1027&a…

2.完整部落格連結:www.shishusheng.com

3.知乎:www.zhihu.com/people/shi-…

4.gayhub:github.com/Wasabi1234

1 執行緒池的好處

執行緒使應用能夠更加充分合理地協調利用CPU、記憶體、網路、I/O等系統資源. 執行緒的建立需要開闢虛擬機器棧、本地方法棧、程式計數器等執行緒私有的記憶體空間; 線上程銷燬時需要回收這些系統資源. 頻繁地建立和銷燬執行緒會浪費大量的系統資源,增加併發程式設計風險.

在伺服器負載過大的時候,如何讓新的執行緒等待或者友好地拒絕服務?

這些都是執行緒自身無法解決的; 所以需要通過執行緒池協調多個執行緒,並實現類似主次執行緒隔離、定時執行、週期執行等任務.

執行緒池的作用包括: ●利用執行緒池管理並複用執行緒、控制最大併發數等 ●實現任務執行緒佇列快取策略和拒絕機制 ●實現某些與時間相關的功能 如定時執行、週期執行等 ●隔離執行緒環境 比如,交易服務和搜尋服務在同一臺伺服器上,分別開啟兩個執行緒池,交易執行緒的資源消耗明顯要大; 因此,通過配置獨立的執行緒池,將較慢的交易服務與搜尋服務隔離開,避免各服務執行緒相互影響.

在開發中,合理地使用執行緒池能夠帶來3個好處

  • 降低資源消耗 通過重複利用已建立的執行緒,降低建立和銷燬執行緒造成的系統資源消耗
  • 提高響應速度 當任務到達時,任務可以不需要等到執行緒建立就能立即執行
  • 提高執行緒的可管理性 執行緒是稀缺資源,如果過多地建立,不僅會消耗系統資源,還會降低系統的穩定性,導致使用執行緒池可以進行統一分配、調優和監控。

在瞭解執行緒池的基本作用後,我們學習一下執行緒池是如何建立執行緒的

2 建立執行緒池

首先從ThreadPoolExecutor構造方法講起,學習如何自定義ThreadFactoryRejectedExecutionHandler; 並編寫一個最簡單的執行緒池示例. 然後,通過分析ThreadPoolExecutorexecuteaddWorker兩個核心方法; 學習如何把任務執行緒加入到執行緒池中執行.

  • ThreadPoolExecutor 的構造方法如下

    Java原始碼解析 - ThreadPoolExecutor 執行緒池

  • 第1個引數: corePoolSize 表示常駐核心執行緒數 如果等於0,則任務執行完之後,沒有任何請求進入時銷燬執行緒池的執行緒; 如果大於0,即使本地任務執行完畢,核心執行緒也不會被銷燬. 這個值的設定非常關鍵; 設定過大會浪費資源; 設定過小會導致執行緒頻繁地建立或銷燬.

  • 第2個引數: maximumPoolSize 表示執行緒池能夠容納同時執行的最大執行緒數 從第1處來看,必須>=1. 如果待執行的執行緒數大於此值,需要藉助第5個引數的幫助,快取在佇列中. 如果maximumPoolSize = corePoolSize,即是固定大小執行緒池.

  • 第3個引數: keepAliveTime 表示執行緒池中的執行緒空閒時間 當空閒時間達到keepAliveTime時,執行緒會被銷燬,直到只剩下corePoolSize個執行緒; 避免浪費記憶體和控制程式碼資源. 在預設情況下,當執行緒池的執行緒數大於corePoolSize時,keepAliveTime才起作用. 但是當ThreadPoolExecutorallowCoreThreadTimeOut = true時,核心執行緒超時後也會被回收.

  • 第4個引數: TimeUnit表示時間單位 keepAliveTime的時間單位通常是TimeUnit.SECONDS.

  • 第5個引數: workQueue 表示快取佇列 當請求的執行緒數大於maximumPoolSize時,執行緒進入BlockingQueue. 後續示例程式碼中使用的LinkedBlockingQueue是單向連結串列,使用鎖來控制入隊和出隊的原子性; 兩個鎖分別控制元素的新增和獲取,是一個生產消費模型佇列.

  • 第6個引數: threadFactory 表示執行緒工廠 它用來生產一組相同任務的執行緒; 執行緒池的命名是通過給這個factory增加組名字首來實現的. 在虛擬機器棧分析時,就可以知道執行緒任務是由哪個執行緒工廠產生的.

  • 第7個引數: handler 表示執行拒絕策略的物件 當超過第5個引數workQueue的任務快取區上限的時候,就可以通過該策略處理請求,這是一種簡單的限流保護. 友好的拒絕策略可以是如下三種: (1 ) 儲存到資料庫進行削峰填谷;在空閒時再提取出來執行 (2)轉向某個提示頁面 (3)列印日誌

2.1.1 corePoolSize(核心執行緒數量)

執行緒池中應該保持的主要執行緒的數量.即使執行緒處於空閒狀態,除非設定了allowCoreThreadTimeOut這個引數,當提交一個任務到執行緒池時,若執行緒數量<corePoolSize,執行緒池會建立一個新執行緒放入works(一個HashSet)中執行任務,即使其他空閒的基本執行緒能夠執行新任務也還是會建立新執行緒 等到需要執行的任務數大於執行緒池基本大小時就不再建立,會嘗試放入等待佇列workQueue 如果呼叫執行緒池的prestartAllCoreThreads(),執行緒池會提前建立並啟動所有核心執行緒

Java原始碼解析 - ThreadPoolExecutor 執行緒池

2.1.2 maximumPoolSize(執行緒池最大執行緒數)

執行緒池允許建立的最大執行緒數 若佇列滿,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒放入works中執行任務,CashedThreadPool的關鍵,固定執行緒數的執行緒池無效 若使用了無界任務佇列,這個引數就沒什麼效果

  • workQueue 儲存待執行任務的阻塞佇列,這些任務必須是Runnable的物件(如果是Callable物件,會在submit內部轉換為Runnable物件)

  • runnableTaskQueue(任務佇列):用於儲存等待執行的任務的阻塞佇列.可以選擇以下幾個阻塞佇列.

    • LinkedBlockingQueue:一個基於連結串列結構的阻塞佇列,此佇列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue.靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列
    • SynchronousQueue:一個不儲存元素的阻塞佇列.每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於Linked-BlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列
  • ThreadFactory:用於設定建立執行緒的工廠,可以通過執行緒工廠給每個建立出來的執行緒設定更有意義的名字.使用開源框架guava提供ThreadFactoryBuilder可以快速給執行緒池裡的執行緒設定有意義的名字,程式碼如下

new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
複製程式碼
  • RejectedExecutionHandler(拒絕策略) 當佇列和執行緒池都滿,說明執行緒池飽和,必須採取一種策略處理提交的新任務 策略預設AbortPolicy,表無法處理新任務時丟擲異常 在JDK 1.5中Java執行緒池框架提供了以下4種策略
    • AbortPolicy:丟棄任務,丟擲 RejectedExecutionException
    • CallerRunsPolicy:只用呼叫者所線上程來執行任務,有反饋機制,使任務提交的速度變慢)。
    • DiscardOldestPolicy 若沒有發生shutdown,嘗試丟棄佇列裡最近的一個任務,並執行當前任務, 丟棄任務快取佇列中最老的任務,並且嘗試重新提交新的任務
    • DiscardPolicy:不處理,丟棄掉, 拒絕執行,不拋異常 當然,也可以根據應用場景需要來實現RejectedExecutionHandler介面自定義策略.如記錄日誌或持久化儲存不能處理的任務
   /**
     * Invokes the rejected execution handler for the given command.
     * Package-protected for use by ScheduledThreadPoolExecutor.
     */
    final void reject(Runnable command) {
        // 執行拒絕策略
        handler.rejectedExecution(command, this);
    }
複製程式碼

handler 構造執行緒池時候就傳的引數,RejectedExecutionHandler的例項 RejectedExecutionHandlerThreadPoolExecutor 中有四個實現類可供我們直接使用,當然,也可以實現自己的策略,一般也沒必要。

    //只要執行緒池沒有被關閉,由提交任務的執行緒自己來執行這個任務
    public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

    // 不管怎樣,直接丟擲 RejectedExecutionException 異常
    // 預設的策略,如果我們構造執行緒池的時候不傳相應的 handler ,則指定使用這個
    public static class AbortPolicy implements RejectedExecutionHandler {
       
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

    // 不做任何處理,直接忽略掉這個任務
    public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

    // 若執行緒池未被關閉
    // 把佇列隊頭的任務(也就是等待了最長時間的)直接扔掉,然後提交這個任務到等待佇列中
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
      
        public DiscardOldestPolicy() { }

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }
複製程式碼
  • keepAliveTime(執行緒活動保持時間) 執行緒沒有任務執行時最多保持多久時間終止 執行緒池的工作執行緒空閒後,保持存活的時間。 所以,如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高執行緒的利用率

  • TimeUnit(執行緒活動保持時間的單位):指示第三個引數的時間單位;可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS,千分之一微秒)

從程式碼第2處來看,佇列、執行緒工廠、拒絕處理服務都必須有例項物件; 但在實際程式設計中,很少有程式設計師對這三者進行例項化,而通過Executors這個執行緒池靜態工廠提供預設實現; 那麼Exceutors與ThreadPoolExecutor是什麼關係呢?

執行緒池相關類圖

Executors工廠類

ExecutorService 的抽象類AbstractExecutorService提供了submitinvokeAll 等方法的實現; 但是核心方法Executor.execute()並沒有在這裡實現. 因為所有的任務都在該方法執行,不同實現會帶來不同的執行策略.

通過Executors的靜態工廠方法可以建立三個執行緒池的包裝物件

  • ForkJoinPool、
  • ThreadPoolExecutor
  • ScheduledThreadPoolExecutor

● Executors.newWorkStealingPool JDK8 引入,建立持有足夠執行緒的執行緒池支援給定的並行度; 並通過使用多個佇列減少競爭; 構造方法中把CPU數量設定為預設的並行度. 返回ForkJoinPool ( JDK7引入)物件,它也是AbstractExecutorService 的子類

Java原始碼解析 - ThreadPoolExecutor 執行緒池

● Executors.newCachedThreadPool maximumPoolSize 最大可以至Integer.MAX_VALUE,是高度可伸縮的執行緒池. 若達到該上限,相信沒有伺服器能夠繼續工作,直接OOM. keepAliveTime 預設為60秒; 工作執行緒處於空閒狀態,則回收工作執行緒; 如果任務數增加,再次建立出新執行緒處理任務.

● Executors.newScheduledThreadPool 執行緒數最大至Integer.MAX_ VALUE,與上述相同,存在OOM風險. ScheduledExecutorService介面的實現類,支援定時及週期性任務執行; 相比Timer,ScheduledExecutorService 更安全,功能更強大. 與newCachedThreadPool的區別是不回收工作執行緒.

● Executors.newSingleThreadExecutor 建立一個單執行緒的執行緒池,相當於單執行緒序列執行所有任務,保證按任務的提交順序依次執行.

● Executors.newFixedThreadPool 輸入的引數即是固定執行緒數; 既是核心執行緒數也是最大執行緒數; 不存在空閒執行緒,所以keepAliveTime等於0.

Java原始碼解析 - ThreadPoolExecutor 執行緒池
其中使用了 LinkedBlockingQueue, 但是沒有設定上限!!!,堆積過多工!!!

下面介紹LinkedBlockingQueue的構造方法

Java原始碼解析 - ThreadPoolExecutor 執行緒池
使用這樣的無界佇列,如果瞬間請求非常大,會有OOM的風險; 除newWorkStealingPool 外,其他四個建立方式都存在資源耗盡的風險.

不推薦使用其中的任何建立執行緒池的方法,因為都沒有任何限制,存在安全隱患.

Executors中預設的執行緒工廠和拒絕策略過於簡單,通常對使用者不夠友好. 執行緒工廠需要做建立前的準備工作,對執行緒池建立的執行緒必須明確標識,就像藥品的生產批號一樣,為執行緒本身指定有意義的名稱和相應的序列號. 拒絕策略應該考慮到業務場景,返回相應的提示或者友好地跳轉. 以下為簡單的ThreadFactory 示例

Java原始碼解析 - ThreadPoolExecutor 執行緒池

上述示例包括執行緒工廠和任務執行體的定義; 通過newThread方法快速、統一地建立執行緒任務,強調執行緒一定要有特定意義的名稱,方便出錯時回溯.

  • 單執行緒池:newSingleThreadExecutor()方法建立,五個引數分別是ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())。含義是池中保持一個執行緒,最多也只有一個執行緒,也就是說這個執行緒池是順序執行任務的,多餘的任務就在佇列中排隊。
  • 固定執行緒池:newFixedThreadPool(nThreads)方法建立
    Java原始碼解析 - ThreadPoolExecutor 執行緒池
    池中保持nThreads個執行緒,最多也只有nThreads個執行緒,多餘的任務也在佇列中排隊。
    ThreadPoolExecutor的構造器-1
    ThreadPoolExecutor的構造器-2

執行緒數固定且執行緒不超時

  • 快取執行緒池:newCachedThreadPool()建立,五個引數分別是ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue())。 含義是池中不保持固定數量的執行緒,隨需建立,最多可以建立Integer.MAX_VALUE個執行緒(說一句,這個數量已經大大超過目前任何作業系統允許的執行緒數了),空閒的執行緒最多保持60秒,多餘的任務在SynchronousQueue(所有阻塞、併發佇列在後續文章中具體介紹)中等待。

為什麼單執行緒池和固定執行緒池使用的任務阻塞佇列是LinkedBlockingQueue(),而快取執行緒池使用的是SynchronousQueue()呢? 因為單執行緒池和固定執行緒池中,執行緒數量是有限的,因此提交的任務需要在LinkedBlockingQueue佇列中等待空餘的執行緒;而快取執行緒池中,執行緒數量幾乎無限(上限為Integer.MAX_VALUE),因此提交的任務只需要在SynchronousQueue佇列中同步移交給空餘執行緒即可。

  • 單執行緒排程執行緒池:newSingleThreadScheduledExecutor()建立,五個引數分別是 (1, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue())。含義是池中保持1個執行緒,多餘的任務在DelayedWorkQueue中等待。
  • 固定排程執行緒池:newScheduledThreadPool(n)建立,五個引數分別是 (n, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue())。含義是池中保持n個執行緒,多餘的任務在DelayedWorkQueue中等待。

有一項技術可以緩解執行時間較長任務造成的影響,即限定任務等待資源的時間,而不要無限的等待

先看第一個例子,測試單執行緒池、固定執行緒池和快取執行緒池(注意增加和取消註釋):

public class ThreadPoolExam {
    public static void main(String[] args) {
        //first test for singleThreadPool
        ExecutorService pool = Executors.newSingleThreadExecutor();
        //second test for fixedThreadPool
//        ExecutorService pool = Executors.newFixedThreadPool(2);
        //third test for cachedThreadPool
//        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            pool.execute(new TaskInPool(i));
        }
        pool.shutdown();
    }
}

class TaskInPool implements Runnable {
    private final int id;

    TaskInPool(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("TaskInPool-["+id+"] is running phase-"+i);
                TimeUnit.SECONDS.sleep(1);
            }
            System.out.println("TaskInPool-["+id+"] is over");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

如圖為排查底層公共快取呼叫出錯時的截圖

有意義的執行緒命名
綠色框採用自定義的執行緒工廠,明顯比藍色框預設的執行緒工廠建立的執行緒名稱擁有更多的額外資訊:如呼叫來源、執行緒的業務含義,有助於快速定位到死鎖、StackOverflowError 等問題.

拒絕策略

下面再簡單地實現一下RejectedExecutionHandler; 實現了介面的rejectedExecution方法,列印出當前執行緒池狀態

Java原始碼解析 - ThreadPoolExecutor 執行緒池

ThreadPoolExecutor中提供了四個公開的內部靜態類 ● AbortPolicy - 預設 丟棄任務並丟擲RejectedExecutionException

Java原始碼解析 - ThreadPoolExecutor 執行緒池

● DiscardPolicy - 不推薦 丟棄任務,但不拋異常.

Java原始碼解析 - ThreadPoolExecutor 執行緒池

● DiscardOldestPolicy 拋棄佇列中等待最久的任務,然後把當前任務加入佇列中.

Java原始碼解析 - ThreadPoolExecutor 執行緒池

● CallerRunsPolicy 呼叫任務的run()方法繞過執行緒池直接執行.

Java原始碼解析 - ThreadPoolExecutor 執行緒池

根據之前實現的執行緒工廠和拒絕策略,執行緒池的相關程式碼實現如下

Java原始碼解析 - ThreadPoolExecutor 執行緒池
Java原始碼解析 - ThreadPoolExecutor 執行緒池
當任務被拒絕的時候,拒絕策略會列印出當前執行緒池的大小已經達到了maximumPoolSize=2,且佇列已滿,完成的任務數提示已經有1個(最後一行).

原始碼講解

ThreadPoolExecutor的屬性定義中頻繁地用位運算來表示執行緒池狀態; 位運算是改變當前值的一種高效手段.

下面從屬性定義開始

Integer 有32位; 最右邊29位表工作執行緒數; 最左邊3位表示執行緒池狀態,可表示從0至7的8個不同數值 執行緒池的狀態用高3位表示,其中包括了符號位. 五種狀態的十進位制值按從小到大依次排序為 RUNNING < SHUTDOWN < STOP < TIDYING <TERMINATED 這樣設計的好處是可以通過比較值的大小來確定執行緒池的狀態. 例如程式中經常會出現isRunning的判斷:

Java原始碼解析 - ThreadPoolExecutor 執行緒池

Java原始碼解析 - ThreadPoolExecutor 執行緒池

  • 000-1111111111111111111111111; 類似於子網掩碼,用於與運算; 得到左邊3位,還是右邊29位
    Java原始碼解析 - ThreadPoolExecutor 執行緒池

用左邊3位,實現5種執行緒池狀態; 在左3位之後加入中畫線有助於理解;

  • 111 - 0000000000000000000000000000(十進位制: -536, 870, 912); 該狀態表 執行緒池能接受新任務

    Java原始碼解析 - ThreadPoolExecutor 執行緒池

  • 000 - 0000000000000000000000000(十進位制: 0); 此狀態不再接受新任務,但可繼續執行佇列中的任務

    Java原始碼解析 - ThreadPoolExecutor 執行緒池

  • 001 - 00000000000000000000000000(十進位制: 536,870, 912); 此狀態全面拒絕,並中斷正在處理的任務

    Java原始碼解析 - ThreadPoolExecutor 執行緒池

  • 010 - 00000000000000000000000000.(十進位制值: 1, 073, 741, 824); 該狀態表 所有任務已經被終止

    Java原始碼解析 - ThreadPoolExecutor 執行緒池

  • 101 - 000000000000000000000000000(十進位制值: 1, 610,612, 736) 該狀態表 已清理完現場

    Java原始碼解析 - ThreadPoolExecutor 執行緒池

與運算,比如 001 - 000000000000000000000100011 表 67個工作執行緒; 掩碼取反: 111 - 00000000000000000000000.,即得到左邊3位001; 表示執行緒池當前處於STOP狀態

Java原始碼解析 - ThreadPoolExecutor 執行緒池

同理掩碼 000 - 11111111111111111111,得到右邊29位,即工作執行緒數

Java原始碼解析 - ThreadPoolExecutor 執行緒池

把左3位與右29位或運算,合併成一個值

Java原始碼解析 - ThreadPoolExecutor 執行緒池

我們都知道Executor介面有且只有一個方法execute(); 通過引數傳入待執行執行緒的物件. 下面分析ThreadPoolExecutor關於execute()方法的實現

執行緒池執行任務的方法如下

   /**
     * Executes the given task sometime in the future.  The task
     * may execute in a new thread or in an existing pooled thread.
     *
     * If the task cannot be submitted for execution, either because this
     * executor has been shutdown or because its capacity has been reached,
     * the task is handled by the current {@code RejectedExecutionHandler}.
     *
     * @param command the task to execute
     * @throws RejectedExecutionException at discretion of
     *         {@code RejectedExecutionHandler}, if the task
     *         cannot be accepted for execution
     * @throws NullPointerException if {@code command} is null
     */
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        // 返回包含執行緒數及執行緒池狀態的Integer 型別數值
        int c = ctl.get();

        // 若工作執行緒數 < 核心執行緒數,則建立執行緒並執行當前任務
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
複製程式碼

execute方法在不同的階段有三次addWorker的嘗試動作。

                return;
            // 若建立失敗,為防止外部已經線上程池中加入新任務,在此重新獲取一下
            c = ctl.get();
        }

        // 若 工作執行緒數 >=核心執行緒數 或執行緒建立失敗,則將當前任務放到工作佇列中
        // 只有執行緒池處於 RUNNING 態,才執行後半句 : 置入佇列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            
            // 只有執行緒池處於 RUNNING 態,才執行後半句 : 置入佇列
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 若之前的執行緒已被消費完,新建一個執行緒
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        // 核心執行緒和佇列都已滿,嘗試建立一個新執行緒
        }
        else if (!addWorker(command, false))
            // 丟擲RejectedExecutionException異常
            // 若 addWorker 返回是 false,即建立失敗,則喚醒拒絕策略.
            reject(command);
    }
複製程式碼

發生拒絕的理由有兩個 ( 1 )執行緒池狀態為非RUNNING狀態 (2)等待佇列已滿。

下面繼續分析addWorker

addWorker 原始碼解析

根據當前執行緒池狀態,檢查是否可以新增新的任務執行緒,若可以則建立並啟動任務; 若一切正常則返回true; 返回false的可能性如下

  1. 執行緒池沒有處於RUNNING
  2. 執行緒工廠建立新的任務執行緒失敗

引數

  • firstTask 外部啟動執行緒池時需要構造的第一個執行緒,它是執行緒的母體
  • core 新增工作執行緒時的判斷指標
    • true 需要判斷當前RUNNING態的執行緒是否少於corePoolsize
    • false 需要判斷當前RUNNING態的執行緒是否少於maximumPoolsize
      Java原始碼解析 - ThreadPoolExecutor 執行緒池
      Java原始碼解析 - ThreadPoolExecutor 執行緒池
      Java原始碼解析 - ThreadPoolExecutor 執行緒池

這段程式碼晦澀難懂,部分地方甚至違反程式碼規約,但其中蘊含豐富的編碼知識點

  • 第1處,配合迴圈語句出現的label,類似於goto 作用 label 定義時,必須把標籤和冒號的組合語句緊緊相鄰定義在迴圈體之前,否則會編譯出錯. 目的是 在實現多重迴圈時能夠快速退出到任何一層; 出發點似乎非常貼心,但在大型軟體專案中,濫用標籤行跳轉的後果將是災難性的. 示例程式碼中在retry下方有兩個無限迴圈; 在workerCount加1成功後,直接退出兩層迴圈.

  • 第2處,這樣的表示式不利於閱讀,應如是

    Java原始碼解析 - ThreadPoolExecutor 執行緒池

  • 第3處,與第1處的標籤呼應,AtomicInteger物件的加1操作是原子性的; break retry表 直接跳出與retry 相鄰的這個迴圈體

  • 第4處,此continue跳轉至標籤處,繼續執行迴圈. 如果條件為false,則說明執行緒池還處於執行狀態,即繼續在for(;)迴圈內執行.

  • 第5處,compareAndIncrementWorkerCount方法執行失敗的概率非常低. 即使失敗,再次執行時成功的概率也是極高的,類似於自旋原理. 這裡是先加1,建立失敗再減1,這是輕量處理併發建立執行緒的方式; 如果先建立執行緒,成功再加1,當發現超出限制後再銷燬執行緒,那麼這樣的處理方式明顯比前者代價要大.

  • 第6處,Worker物件是工作執行緒的核心類實現,部分原始碼如下

    Java原始碼解析 - ThreadPoolExecutor 執行緒池
    它實現了Runnable介面,並把本物件作為引數輸入給run()中的runWorker (this); 所以內部屬性執行緒threadstart的時候,即會呼叫runWorker.

總結

執行緒池的相關原始碼比較精煉,還包括執行緒池的銷燬、任務提取和消費等,與執行緒狀態圖一樣,執行緒池也有自己獨立的狀態轉化流程,本節不再展開。 總結一下,使用執行緒池要注意如下幾點: (1)合理設定各類引數,應根據實際業務場景來設定合理的工作執行緒數。 (2)執行緒資源必須通過執行緒池提供,不允許在應用中自行顯式建立執行緒。 (3)建立執行緒或執行緒池時請指定有意義的執行緒名稱,方便出錯時回溯。

執行緒池不允許使用Executors,而是通過ThreadPoolExecutor的方式建立,這樣的處理方式能更加明確執行緒池的執行規則,規避資源耗盡的風險。

進一步檢視原始碼發現,這些方法最終都呼叫了ThreadPoolExecutor和ScheduledThreadPoolExecutor的建構函式 而ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor

0.2 ThreadPoolExecutor 自定義執行緒池

ExecutorService介面的兩個主要的具體實現
它們都是某種執行緒池,可以控制執行緒建立,釋放,並通過某種策略嘗試複用執行緒去執行任務的一個管理框架

,因此最終所有執行緒池的建構函式都呼叫了Java5後推出的ThreadPoolExecutor的如下建構函式

Java原始碼解析 - ThreadPoolExecutor 執行緒池

Java預設提供的執行緒池

Java中的執行緒池是運用場景最多的併發框架,幾乎所有需要非同步或併發執行任務的程式都可以使用執行緒池

Java原始碼解析 - ThreadPoolExecutor 執行緒池
Java原始碼解析 - ThreadPoolExecutor 執行緒池

我們只需要將待執行的方法放入 run 方法中,將 Runnable 介面的實現類交給執行緒池的 execute 方法,作為他的一個引數,比如:

Executor e=Executors.newSingleThreadExecutor();           
e.execute(new Runnable(){ //匿名內部類     public  void run(){  
//需要執行的任務 
} 
}); 

複製程式碼

執行緒池的實現原理

Java原始碼解析 - ThreadPoolExecutor 執行緒池
Java原始碼解析 - ThreadPoolExecutor 執行緒池

ThreadPoolExecutor執行execute()分4種情況

  • 若當前執行的執行緒少於corePoolSize,則建立新執行緒來執行任務(執行這一步需要獲取全域性鎖)
  • 若執行的執行緒多於或等於corePoolSize,則將任務加入BlockingQueue
  • 若無法將任務加入BlockingQueue,則建立新的執行緒來處理任務(執行這一步需要獲取全域性鎖)
  • 若建立新執行緒將使當前執行的執行緒超出maximumPoolSize,任務將被拒絕,並呼叫RejectedExecutionHandler.rejectedExecution()

採取上述思路,是為了在執行execute()時,儘可能避免獲取全域性鎖 在ThreadPoolExecutor完成預熱之後(當前執行的執行緒數大於等於corePoolSize),幾乎所有的execute()方法呼叫都是執行步驟2,而步驟2不需要獲取全域性鎖

#原始碼分析

   /**
	 * 檢查是否可以根據當前池狀態和給定的邊界(核心或最大)
     * 新增新工作執行緒。如果是這樣,工作執行緒數量會相應調整,如果可能的話,一個新的工作執行緒建立並啟動
     * 將firstTask作為其執行的第一項任務。
     * 如果池已停止此方法返回false
     * 如果執行緒工廠在被訪問時未能建立執行緒,也返回false
     * 如果執行緒建立失敗,或者是由於執行緒工廠返回null,或者由於異常(通常是在呼叫Thread.start()後的OOM)),我們乾淨地回滾。
	 *
	 * @param core if true use corePoolSize as bound, else
	 * maximumPoolSize. (A boolean indicator is used here rather than a
	 * value to ensure reads of fresh values after checking other pool
	 * state).
	 * @return true if successful
	 */
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
      
            
    /**
     * Check if queue empty only if necessary.
     * 
     * 如果執行緒池已關閉,並滿足以下條件之一,那麼不建立新的 worker:
     *      1. 執行緒池狀態大於 SHUTDOWN,也就是 STOP, TIDYING, 或 TERMINATED
     *      2. firstTask != null
     *      3. workQueue.isEmpty()
     * 簡單分析下:
     *      狀態控制的問題,當執行緒池處於 SHUTDOWN ,不允許提交任務,但是已有任務繼續執行
     *      當狀態大於 SHUTDOWN ,不允許提交任務,且中斷正在執行任務
     *      多說一句:若執行緒池處於 SHUTDOWN,但 firstTask 為 null,且 workQueue 非空,是允許建立 worker 的
     *  
     */
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                // 如果成功,那麼就是所有建立執行緒前的條件校驗都滿足了,準備建立執行緒執行任務
                // 這裡失敗的話,說明有其他執行緒也在嘗試往執行緒池中建立執行緒
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                // 由於有併發,重新再讀取一下 ctl
                c = ctl.get();  // Re-read ctl
                // 正常如果是 CAS 失敗的話,進到下一個裡層的for迴圈就可以了
                // 可如果是因為其他執行緒的操作,導致執行緒池的狀態發生了變更,如有其他執行緒關閉了這個執行緒池
                // 那麼需要回到外層的for迴圈
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

     /* *
        * 到這裡,我們認為在當前這個時刻,可以開始建立執行緒來執行任務
        */
         
        // worker 是否已經啟動
        boolean workerStarted = false;
        // 是否已將這個 worker 新增到 workers 這個 HashSet 中
        boolean workerAdded = false;
        Worker w = null;
        try {
           // 把 firstTask 傳給 worker 的構造方法
            w = new Worker(firstTask);
            // 取 worker 中的執行緒物件,Worker的構造方法會呼叫 ThreadFactory 來建立一個新的執行緒
            final Thread t = w.thread;
            if (t != null) {
               //先加鎖
                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());

                    // 小於 SHUTTDOWN 即 RUNNING
                    // 如果等於 SHUTDOWN,不接受新的任務,但是會繼續執行等待佇列中的任務
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        // worker 裡面的 thread 不能是已啟動的
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        // 加到 workers 這個 HashSet 中
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
               // 若新增成功
                if (workerAdded) {
                    // 啟動執行緒
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            // 若執行緒沒有啟動,做一些清理工作,若前面 workCount 加了 1,將其減掉
            if (! workerStarted)
                addWorkerFailed(w);
        }
        // 返回執行緒是否啟動成功
        return workerStarted;
    }
複製程式碼

看下 addWorkFailed

workers 中刪除掉相應的 worker,workCount 減 1
private void addWor

記錄 workers 中的個數的最大值,因為 workers 是不斷增加減少的,通過這個值可以知道執行緒池的大小曾經達到的最大值
Java原始碼解析 - ThreadPoolExecutor 執行緒池

worker 中的執行緒 start 後,其 run 方法會呼叫 runWorker

Java原始碼解析 - ThreadPoolExecutor 執行緒池
繼續往下看 runWorker

//  worker 執行緒啟動後呼叫,while 迴圈(即自旋!)不斷從等待佇列獲取任務並執行
//  worker 初始化時,可指定 firstTask,那麼第一個任務也就可以不需要從佇列中獲取
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    // 該執行緒的第一個任務(若有)
    Runnable task = w.firstTask;
    w.firstTask = null;
    // 允許中斷
    w.unlock(); 
 
    boolean completedAbruptly = true;
    try {
        // 迴圈呼叫 getTask 獲取任務
        while (task != null || (task = getTask()) != null) {
            w.lock();          
            // 若執行緒池狀態大於等於 STOP,那麼意味著該執行緒也要中斷
              /**
               * 若執行緒池STOP,請確保執行緒 已被中斷
               * 如果沒有,請確保執行緒未被中斷
               * 這需要在第二種情況下進行重新檢查,以便在關中斷時處理shutdownNow競爭
               */
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                // 這是一個鉤子方法,留給需要的子類實現
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 到這裡終於可以執行任務了
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    // 這裡不允許丟擲 Throwable,所以轉換為 Error
                    thrown = x; throw new Error(x);
                } finally {
                    // 也是一個鉤子方法,將 task 和異常作為引數,留給需要的子類實現
                    afterExecute(task, thrown);
                }
            } finally {
                // 置空 task,準備 getTask 下一個任務
                task = null;
                // 累加完成的任務數
                w.completedTasks++;
                // 釋放掉 worker 的獨佔鎖
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 到這裡,需要執行執行緒關閉
        // 1. 說明 getTask 返回 null,也就是說,這個 worker 的使命結束了,執行關閉
        // 2. 任務執行過程中發生了異常
        //    第一種情況,已經在程式碼處理了將 workCount 減 1,這個在 getTask 方法分析中說
        //    第二種情況,workCount 沒有進行處理,所以需要在 processWorkerExit 中處理
        processWorkerExit(w, completedAbruptly);
    }
}
複製程式碼

看看 getTask()

Java原始碼解析 - ThreadPoolExecutor 執行緒池

// 此方法有三種可能
// 1. 阻塞直到獲取到任務返回。預設 corePoolSize 之內的執行緒是不會被回收的,它們會一直等待任務
// 2. 超時退出。keepAliveTime 起作用的時候,也就是如果這麼多時間內都沒有任務,那麼應該執行關閉
// 3. 如果發生了以下條件,須返回 null
//     池中有大於 maximumPoolSize 個 workers 存在(通過呼叫 setMaximumPoolSize 進行設定)
//     執行緒池處於 SHUTDOWN,而且 workQueue 是空的,前面說了,這種不再接受新的任務
//     執行緒池處於 STOP,不僅不接受新的執行緒,連 workQueue 中的執行緒也不再執行
private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

   for (;;) {
   // 允許核心執行緒數內的執行緒回收,或當前執行緒數超過了核心執行緒數,那麼有可能發生超時關閉
 
            // 這裡 break,是為了不往下執行後一個 if (compareAndDecrementWorkerCount(c))
            // 兩個 if 一起看:如果當前執行緒數 wc > maximumPoolSize,或者超時,都返回 null
            // 那這裡的問題來了,wc > maximumPoolSize 的情況,為什麼要返回 null?
            // 換句話說,返回 null 意味著關閉執行緒。
            // 那是因為有可能開發者呼叫了 setMaximumPoolSize 將執行緒池的 maximumPoolSize 調小了
        
            // 如果此 worker 發生了中斷,採取的方案是重試
            // 解釋下為什麼會發生中斷,這個讀者要去看 setMaximumPoolSize 方法,
            // 如果開發者將 maximumPoolSize 調小了,導致其小於當前的 workers 數量,
            // 那麼意味著超出的部分執行緒要被關閉。重新進入 for 迴圈,自然會有部分執行緒會返回 null
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                // CAS 操作,減少工作執行緒數
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
            // 如果此 worker 發生了中斷,採取的方案是重試
            // 解釋下為什麼會發生中斷,這個讀者要去看 setMaximumPoolSize 方法,
            // 如果開發者將 maximumPoolSize 調小了,導致其小於當前的 workers 數量,
            // 那麼意味著超出的部分執行緒要被關閉。重新進入 for 迴圈,自然會有部分執行緒會返回 null
                timedOut = false;
            }
        }
}
複製程式碼

到這裡,基本上也說完了整個流程,回到 execute(Runnable command) 方法,看看各個分支,我把程式碼貼過來一下:

/**
     * Executes the given task sometime in the future.  The task
     * may execute in a new thread or in an existing pooled thread.
     *
     * If the task cannot be submitted for execution, either because this
     * executor has been shutdown or because its capacity has been reached,
     * the task is handled by the current {@code RejectedExecutionHandler}.
     *
     * @param command the task to execute
     * @throws RejectedExecutionException at discretion of
     *         {@code RejectedExecutionHandler}, if the task
     *         cannot be accepted for execution
     * @throws NullPointerException if {@code command} is null
     */
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        //表示 “執行緒池狀態” 和 “執行緒數” 的整數
        int c = ctl.get();
        // 如果當前執行緒數少於核心執行緒數,直接新增一個 worker 執行任務,
        // 建立一個新的執行緒,並把當前任務 command 作為這個執行緒的第一個任務(firstTask)
        if (workerCountOf(c) < corePoolSize) {
        // 新增任務成功,即結束
        // 執行的結果,會包裝到 FutureTask 
        // 返回 false 代表執行緒池不允許提交任務
            if (addWorker(command, true))
                return;
           
            c = ctl.get();
        }

        // 到這說明,要麼當前執行緒數大於等於核心執行緒數,要麼剛剛 addWorker 失敗
  
        // 如果執行緒池處於 RUNNING ,把這個任務新增到任務佇列 workQueue 中
        if (isRunning(c) && workQueue.offer(command)) {
            /* 若任務進入 workQueue,我們是否需要開啟新的執行緒
             * 執行緒數在 [0, corePoolSize) 是無條件開啟新執行緒的
             * 若執行緒數已經大於等於 corePoolSize,則將任務新增到佇列中,然後進到這裡
             */
            int recheck = ctl.get();
            // 若執行緒池不處於 RUNNING ,則移除已經入隊的這個任務,並且執行拒絕策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 若執行緒池還是 RUNNING ,且執行緒數為 0,則開啟新的執行緒
            // 這塊程式碼的真正意圖:擔心任務提交到佇列中了,但是執行緒都關閉了
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 若 workQueue 滿,到該分支
        // 以 maximumPoolSize 為界建立新 worker,
        // 若失敗,說明當前執行緒數已經達到 maximumPoolSize,執行拒絕策略
        else if (!addWorker(command, false))
            reject(command);
    }
複製程式碼

工作執行緒:執行緒池建立執行緒時,會將執行緒封裝成工作執行緒Worker,Worker在執行完任務後,還會迴圈獲取工作佇列裡的任務來執行.我們可以從Worker類的run()方法裡看到這點

  public void run() {
        try {
            Runnable task = firstTask;
            firstTask = null;
            while (task != null || (task = getTask()) != null) {
                runTask(task);
                task = null;
            }
        } finally {
            workerDone(this);
        }
    }
 boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);

            final Thread t = w.thread;
            if (t != null) {
               //先加鎖
                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());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        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)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
複製程式碼

執行緒池中的執行緒執行任務分兩種情況

  • 在execute()方法中建立一個執行緒時,會讓這個執行緒執行當前任務
  • 這個執行緒執行完上圖中 1 的任務後,會反覆從BlockingQueue獲取任務來執行

執行緒池的使用

2.2 向執行緒池提交任務

可以使用兩個方法向執行緒池提交任務

2.2.1 execute()

用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功.通過以下程式碼可知execute()方法輸入的任務是一個Runnable類的例項.

    threadsPool.execute(new Runnable() {
            @Override
            public void run() {
                   // TODO Auto-generated method stub
            }
        });
複製程式碼

從執行結果可以看出,單執行緒池中的執行緒是順序執行的。固定執行緒池(引數為2)中,永遠最多隻有兩個執行緒併發執行。快取執行緒池中,所有執行緒都併發執行。 第二個例子,測試單執行緒排程執行緒池和固定排程執行緒池。

public class ScheduledThreadPoolExam {
    public static void main(String[] args) {
        //first test for singleThreadScheduledPool
        ScheduledExecutorService scheduledPool = Executors.newSingleThreadScheduledExecutor();
        //second test for scheduledThreadPool
//        ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
        for (int i = 0; i < 5; i++) {
            scheduledPool.schedule(new TaskInScheduledPool(i), 0, TimeUnit.SECONDS);
        }
        scheduledPool.shutdown();
    }
}

class TaskInScheduledPool implements Runnable {
    private final int id;

    TaskInScheduledPool(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("TaskInScheduledPool-["+id+"] is running phase-"+i);
                TimeUnit.SECONDS.sleep(1);
            }
            System.out.println("TaskInScheduledPool-["+id+"] is over");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

從執行結果可以看出,單執行緒排程執行緒池和單執行緒池類似,而固定排程執行緒池和固定執行緒池類似。 總結:

  • 如果沒有特殊要求,使用快取執行緒池總是合適的;
  • 如果只能執行一個執行緒,就使用單執行緒池。
  • 如果要執行排程任務,則按需使用排程執行緒池或單執行緒排程執行緒池
  • 如果有其他特殊要求,則可以直接使用ThreadPoolExecutor類的建構函式來建立執行緒池,並自己給定那五個引數。

2.2.2 submit()

用於提交需要返回值的任務.執行緒池會返回一個future型別物件,通過此物件可以判斷任務是否執行成功 並可通過get()獲取返回值,get()會阻塞當前執行緒直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前執行緒一段時間後立即返回,這時候可能任務沒有執行完.

    Future<Object> future = executor.submit(harReturnValuetask);
        try {
            Object s = future.get();
        } catch (InterruptedException e) {
            // 處理中斷異常
        } catch (ExecutionException e) {
            // 處理無法執行任務異常
        } finally {
            // 關閉執行緒池
            executor.shutdown();
        }
複製程式碼

2.3 關閉執行緒池

可通過呼叫執行緒池的shutdownshutdownNow方法來關閉執行緒池. 它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止. 但是它們存在一定的區別

  • shutdownNow首先將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表
  • shutdown只是將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒.

只要呼叫了這兩個關閉方法中的任意一個,isShutdown方法就會返回true. 當所有的任務都已關閉後,才表示執行緒池關閉成功,這時呼叫isTerminaed方法會返回true. 至於應該呼叫哪一種方法,應該由提交到執行緒池的任務的特性決定,通常呼叫shutdown方法來關閉執行緒池,若任務不一定要執行完,則可以呼叫shutdownNow方法. ##2.4 合理配置 要想合理地配置執行緒池,就必須首先分析任務特性,可從以下幾個角度來分析

  • 任務的性質:CPU密集型任務、IO密集型任務和混合型任務
  • 任務的優先順序:高、中和低
  • 任務的執行時間:長、中和短
  • 任務的依賴性:是否依賴其他系統資源,如資料庫連線。

性質不同的任務可以用不同規模的執行緒池分開處理

  • CPU密集型任務 應配置儘可能小的執行緒,配置 N(CPU)+1或者 N(CPU) * 2
  • I/O密集型任務 業務讀取較多,執行緒並不是一直在執行任務,則應配置儘可能多的執行緒 N(CPU)/1 - 阻塞係數(0.8~0.9)
  • 混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於序列執行的吞吐量.如果這兩個任務執行時間相差太大,則沒必要進行分解.

可以通過Runtime.getRuntime().availableProcessors()方法獲得當前裝置的CPU個數.

優先順序不同的任務可以使用PriorityBlockingQueue處理.它可以讓優先順序高 的任務先執行.

注意 如果一直有優先順序高的任務提交到佇列裡,那麼優先順序低的任務可能永遠不能執行

執行時間不同的任務可以交給不同規模的執行緒池來處理,或者可以使用優先順序佇列,讓執行時間短的任務先執行.

依賴資料庫連線池的任務,因為執行緒提交SQL後需要等待資料庫返回結果,等待的時間越長,則CPU空閒時間就越長,那麼執行緒數應該設定得越大,這樣才能更好地利用CPU.

建議使用有界佇列 有界佇列能增加系統的穩定性和預警能力,可以根據需要設大一點,比如幾千. 假如系統裡後臺任務執行緒池的佇列和執行緒池全滿了,不斷丟擲拋棄任務的異常,通過排查發現是資料庫出現了問題,導致執行SQL變得非常緩慢,因為後臺任務執行緒池裡的任務全是需要向資料庫查詢和插入資料的,所以導致執行緒池裡的工作執行緒全部阻塞,任務積壓線上程池裡. 如果我們設定成無界佇列,那麼執行緒池的佇列就會越來越多,有可能會撐滿記憶體,導致整個系統不可用,而不只是後臺任務出現問題.

2.5 執行緒池的監控

如果在系統中大量使用執行緒池,則有必要對執行緒池進行監控,方便在出現問題時,可以根據執行緒池的使用狀況快速定位問題.可通過執行緒池提供的引數進行監控,在監控執行緒池的時候可以使用以下屬性:

  • taskCount:執行緒池需要執行的任務數量
  • completedTaskCount:執行緒池在執行過程中已完成的任務數量,小於或等於taskCount。
  • largestPoolSize:執行緒池裡曾經建立過的最大執行緒數量.通過這個資料可以知道執行緒池是否曾經滿過.如該數值等於執行緒池的最大大小,則表示執行緒池曾經滿過.
  • getPoolSize:執行緒池的執行緒數量.如果執行緒池不銷燬的話,執行緒池裡的執行緒不會自動銷燬,所以這個大小隻增不減.
  • getActiveCount:獲取活動的執行緒數.

通過擴充套件執行緒池進行監控.可以通過繼承執行緒池來自定義執行緒池,重寫執行緒池的 beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和執行緒池關閉前執行一些程式碼來進行監控.例如,監控任務的平均執行時間、最大執行時間和最小執行時間等. 這幾個方法線上程池裡是空方法.

protected void beforeExecute(Thread t, Runnable r) { }
複製程式碼

2.6 執行緒池的狀態

1.當執行緒池建立後,初始為 running 狀態 2.呼叫 shutdown 方法後,處 shutdown 狀態,此時不再接受新的任務,等待已有的任務執行完畢 3.呼叫 shutdownnow 方法後,進入 stop 狀態,不再接受新的任務,並且會嘗試終止正在執行的任務。 4.當處於 shotdown 或 stop 狀態,並且所有工作執行緒已經銷燬,任務快取佇列已清空,執行緒池被設為 terminated 狀態。

#總結

java 執行緒池有哪些關鍵屬性?

  • corePoolSize 到 maximumPoolSize 之間的執行緒會被回收,當然 corePoolSize 的執行緒也可以通過設定而得到回收(allowCoreThreadTimeOut(true))。
  • workQueue 用於存放任務,新增任務的時候,如果當前執行緒數超過了 corePoolSize,那麼往該佇列中插入任務,執行緒池中的執行緒會負責到佇列中拉取任務。
  • keepAliveTime 用於設定空閒時間,如果執行緒數超出了 corePoolSize,並且有些執行緒的空閒時間超過了這個值,會執行關閉這些執行緒的操作
  • rejectedExecutionHandler 用於處理當執行緒池不能執行此任務時的情況,預設有丟擲 RejectedExecutionException 異常、忽略任務、使用提交任務的執行緒來執行此任務和將佇列中等待最久的任務刪除,然後提交此任務這四種策略,預設為丟擲異常。 ##執行緒池中的執行緒建立時機?
  • 如果當前執行緒數少於 corePoolSize,那麼提交任務的時候建立一個新的執行緒,並由這個執行緒執行這個任務;
  • 如果當前執行緒數已經達到 corePoolSize,那麼將提交的任務新增到佇列中,等待執行緒池中的執行緒去佇列中取任務;
  • 如果佇列已滿,那麼建立新的執行緒來執行任務,需要保證池中的執行緒數不會超過 maximumPoolSize,如果此時執行緒數超過了 maximumPoolSize,那麼執行拒絕策略。

##任務執行過程中發生異常怎麼處理? 如果某個任務執行出現異常,那麼執行任務的執行緒會被關閉,而不是繼續接收其他任務。然後會啟動一個新的執行緒來代替它。

##什麼時候會執行拒絕策略?

  • workers 的數量達到了 corePoolSize,任務入隊成功,以此同時執行緒池被關閉了,而且關閉執行緒池並沒有將這個任務出隊,那麼執行拒絕策略。這裡說的是非常邊界的問題,入隊和關閉執行緒池併發執行,讀者仔細看看 execute 方法是怎麼進到第一個 reject(command) 裡面的。
  • workers 的數量大於等於 corePoolSize,準備入隊,可是佇列滿了,任務入隊失敗,那麼準備開啟新的執行緒,可是執行緒數已經達到 maximumPoolSize,那麼執行拒絕策略。

參考

<<碼出高效>>

相關文章