Java原始碼解析 ThreadPoolExecutor 執行緒池

weixin_33797791發表於2019-01-25

1 執行緒池的好處

小編整理了一些java進階學習資料和麵試題,需要資料的請加JAVA高階學習Q群:664389243 這是小編建立的java高階學習交流群,加群一起交流學習深造。群裡也有小編整理的2019年最新最全的java高階學習資料!

執行緒使應用能夠更加充分合理地協調利用CPU、記憶體、網路、I/O等系統資源.

執行緒的建立需要開闢虛擬機器棧、本地方法棧、程式計數器等執行緒私有的記憶體空間;

線上程銷燬時需要回收這些系統資源.

頻繁地建立和銷燬執行緒會浪費大量的系統資源,增加併發程式設計風險.

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

這些都是執行緒自身無法解決的;

所以需要通過執行緒池協調多個執行緒,並實現類似主次執行緒隔離、定時執行、週期執行等任務.

執行緒池的作用包括:

●利用執行緒池管理並複用執行緒、控制最大併發數等

●實現任務執行緒佇列快取策略和拒絕機制

●實現某些與時間相關的功能

如定時執行、週期執行等

●隔離執行緒環境

比如,交易服務和搜尋服務在同一臺伺服器上,分別開啟兩個執行緒池,交易執行緒的資源消耗明顯要大;

因此,通過配置獨立的執行緒池,將較慢的交易服務與搜尋服務隔離開,避免各服務執行緒相互影響.

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

降低資源消耗通過重複利用已建立的執行緒,降低建立和銷燬執行緒造成的系統資源消耗

提高響應速度當任務到達時,任務可以不需要等到執行緒建立就能立即執行

提高執行緒的可管理性執行緒是稀缺資源,如果過多地建立,不僅會消耗系統資源,還會降低系統的穩定性,導致使用執行緒池可以進行統一分配、調優和監控。

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

2 建立執行緒池

首先從 ThreadPoolExecutor構造方法講起,學習如何自定義 ThreadFactory和 RejectedExecutionHandler;

並編寫一個最簡單的執行緒池示例.

然後,通過分析 ThreadPoolExecutor的 execute和 addWorker兩個核心方法;

學習如何把任務執行緒加入到執行緒池中執行.

ThreadPoolExecutor 的構造方法如下

15592734-c4739d2a18b49542

第1個引數: corePoolSize 表示常駐核心執行緒數

如果等於0,則任務執行完之後,沒有任何請求進入時銷燬執行緒池的執行緒;

如果大於0,即使本地任務執行完畢,核心執行緒也不會被銷燬.

這個值的設定非常關鍵;

設定過大會浪費資源;

設定過小會導致執行緒頻繁地建立或銷燬.

第2個引數: maximumPoolSize 表示執行緒池能夠容納同時執行的最大執行緒數

從第1處來看,必須>=1.

如果待執行的執行緒數大於此值,需要藉助第5個引數的幫助,快取在佇列中.

如果 maximumPoolSize=corePoolSize,即是固定大小執行緒池.

第3個引數: keepAliveTime 表示執行緒池中的執行緒空閒時間

當空閒時間達到 keepAliveTime時,執行緒會被銷燬,直到只剩下 corePoolSize個執行緒;

避免浪費記憶體和控制程式碼資源.

在預設情況下,當執行緒池的執行緒數大於 corePoolSize時, keepAliveTime才起作用.

但是當 ThreadPoolExecutor的 allowCoreThreadTimeOut=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="" 如果呼叫執行緒池的=""

15592734-60c62f8323ff3c42

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的例項 RejectedExecutionHandler在 ThreadPoolExecutor 中有四個實現類可供我們直接使用,當然,也可以實現自己的策略,一般也沒必要。

   //只要執行緒池沒有被關閉,由提交任務的執行緒自己來執行這個任務

   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是什麼關係呢?

15592734-b5b86a7ff4ecbad5

Executors工廠類

ExecutorService 的抽象類 AbstractExecutorService提供了 submit、 invokeAll 等方法的實現;

但是核心方法 Executor.execute()並沒有在這裡實現.

因為所有的任務都在該方法執行,不同實現會帶來不同的執行策略.

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

ForkJoinPool、

ThreadPoolExecutor

ScheduledThreadPoolExecutor

● Executors.newWorkStealingPool

JDK8 引入,建立持有足夠執行緒的執行緒池支援給定的並行度;

並通過使用多個佇列減少競爭;

構造方法中把CPU數量設定為預設的並行度.

返回 ForkJoinPool ( JDK7引入)物件,它也是 AbstractExecutorService 的子類

15592734-cfcc8c67abe9f9ee

● 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.

其中使用了 LinkedBlockingQueue, 但是沒有設定上限!!!,堆積過多工!!!

15592734-d987f36a46de3a17

下面介紹 LinkedBlockingQueue的構造方法

使用這樣的無界佇列,如果瞬間請求非常大,會有OOM的風險;

15592734-91592bd186e7a6dd

除 newWorkStealingPool 外,其他四個建立方式都存在資源耗盡的風險.

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

Executors中預設的執行緒工廠和拒絕策略過於簡單,通常對使用者不夠友好.

執行緒工廠需要做建立前的準備工作,對執行緒池建立的執行緒必須明確標識,就像藥品的生產批號一樣,為執行緒本身指定有意義的名稱和相應的序列號.

拒絕策略應該考慮到業務場景,返回相應的提示或者友好地跳轉.

以下為簡單的ThreadFactory 示例

15592734-e039ac3ddac04fa8

上述示例包括執行緒工廠和任務執行體的定義;

通過newThread方法快速、統一地建立執行緒任務,強調執行緒一定要有特定意義的名稱,方便出錯時回溯.

單執行緒池:newSingleThreadExecutor()方法建立,五個引數分別是ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())。含義是池中保持一個執行緒,最多也只有一個執行緒,也就是說這個執行緒池是順序執行任務的,多餘的任務就在佇列中排隊。

固定執行緒池:newFixedThreadPool(nThreads)方法建立

池中保持nThreads個執行緒,最多也只有nThreads個執行緒,多餘的任務也在佇列中排隊。

15592734-7abb80a86868dfb4
15592734-ac1cd05eb2a2fdc1
15592734-1cd2bfda875ab7b2

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

快取執行緒池:newCachedThreadPool()建立,五個引數分別是ThreadPoolExecutor(0, Integer.MAXVALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue())。 含義是池中不保持固定數量的執行緒,隨需建立,最多可以建立Integer.MAXVALUE個執行緒(說一句,這個數量已經大大超過目前任何作業系統允許的執行緒數了),空閒的執行緒最多保持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 等問題.

15592734-c34a2c07358b2329

拒絕策略

下面再簡單地實現一下 RejectedExecutionHandler;

實現了介面的 rejectedExecution方法,列印出當前執行緒池狀態

15592734-ef26a364c521ac0c

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

15592734-6aea3b9e2c7f7d2d

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

15592734-f1275b6f0d571a5c

● DiscardOldestPolicy

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

15592734-ad5d87cf123e2bf6

● CallerRunsPolicy

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

15592734-f6e98e5fee3070fb

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

15592734-67c4e37e0c822daa

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

15592734-e9a4e848d0f2d436

原始碼講解

在 ThreadPoolExecutor的屬性定義中頻繁地用位運算來表示執行緒池狀態;

位運算是改變當前值的一種高效手段.

下面從屬性定義開始

Integer 有32位;

最右邊29位表工作執行緒數;

最左邊3位表示執行緒池狀態,可表示從0至7的8個不同數值

執行緒池的狀態用高3位表示,其中包括了符號位.

五種狀態的十進位制值按從小到大依次排序為

RUNNING < SHUTDOWN < STOP < TIDYING<terminated 這樣設計的好處是可以通過比較值的大小來確定執行緒池的狀態.="" 例如程式中經常會出現isrunning的判斷:=""

15592734-ad2249e8182f8179

000-1111111111111111111111111; 類似於子網掩碼,用於與運算; 得到左邊3位,還是右邊29位

15592734-8e24f6c412e6305a

用左邊3位,實現5種執行緒池狀態;

在左3位之後加入中畫線有助於理解;

111 - 0000000000000000000000000000(十進位制: -536, 870, 912);

該狀態表 執行緒池能接受新任務

15592734-55115b58b8118869

000 - 0000000000000000000000000(十進位制: 0);

此狀態不再接受新任務,但可繼續執行佇列中的任務

15592734-71fab3ede15fd75e

001 - 00000000000000000000000000(十進位制: 536,870, 912);

此狀態全面拒絕,並中斷正在處理的任務

15592734-08cc33297d1cf47a

010 - 00000000000000000000000000.(十進位制值: 1, 073, 741, 824);

該狀態表 所有任務已經被終止

15592734-98dff20bb66c521f

101 - 000000000000000000000000000(十進位制值: 1, 610,612, 736)

該狀態表 已清理完現場

15592734-ed1bebfe5931ddea

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

15592734-e85063a3374ce0a4

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

15592734-1dd7b63f6f68b942

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

15592734-64efbdc8b2b13e7a

我們都知道 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的可能性如下

執行緒池沒有處於RUNNING態

執行緒工廠建立新的任務執行緒失敗

引數

firstTask 外部啟動執行緒池時需要構造的第一個執行緒,它是執行緒的母體

core 新增工作執行緒時的判斷指標

true 需要判斷當前 RUNNING態的執行緒是否少於 corePoolsize

false 需要判斷當前 RUNNING態的執行緒是否少於 maximumPoolsize 

15592734-4f65c4d99da7b950
15592734-0b76ae5aa3d3fb31
15592734-47863988f2daa525

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

第1處,配合迴圈語句出現的label,類似於goto 作用

label 定義時,必須把標籤和冒號的組合語句緊緊相鄰定義在迴圈體之前,否則會編譯出錯.

目的是 在實現多重迴圈時能夠快速退出到任何一層;

出發點似乎非常貼心,但在大型軟體專案中,濫用標籤行跳轉的後果將是災難性的.

示例程式碼中在 retry下方有兩個無限迴圈;

在 workerCount加1成功後,直接退出兩層迴圈.

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

15592734-1cd3324d8b47df6a

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

第4處,此 continue跳轉至標籤處,繼續執行迴圈.

如果條件為false,則說明執行緒池還處於執行狀態,即繼續在 for(;)迴圈內執行.

第5處, compareAndIncrementWorkerCount方法執行失敗的概率非常低.

即使失敗,再次執行時成功的概率也是極高的,類似於自旋原理.

這裡是先加1,建立失敗再減1,這是輕量處理併發建立執行緒的方式;

如果先建立執行緒,成功再加1,當發現超出限制後再銷燬執行緒,那麼這樣的處理方式明顯比前者代價要大.

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

它實現了 Runnable介面,並把本物件作為引數輸入給 run()中的 runWorker(this);

15592734-84335e29b9e657cf

所以內部屬性執行緒 thread在 start的時候,即會呼叫 runWorker.

總結

執行緒池的相關原始碼比較精煉,還包括執行緒池的銷燬、任務提取和消費等,與執行緒狀態圖一樣,執行緒池也有自己獨立的狀態轉化流程,本節不再展開。

總結一下,使用執行緒池要注意如下幾點:

(1)合理設定各類引數,應根據實際業務場景來設定合理的工作執行緒數。

(2)執行緒資源必須通過執行緒池提供,不允許在應用中自行顯式建立執行緒。

(3)建立執行緒或執行緒池時請指定有意義的執行緒名稱,方便出錯時回溯。

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

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

而ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor

0.2 ThreadPoolExecutor 自定義執行緒池

它們都是某種執行緒池,可以控制執行緒建立,釋放,並通過某種策略嘗試複用執行緒去執行任務的一個管理框架

15592734-5832a53fb71c0498

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

15592734-f97595f55feefafe

Java預設提供的執行緒池

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

15592734-13b00c2781e936b7
15592734-f2ffa5122925d652

我們只需要將待執行的方法放入 run 方法中,將 Runnable 介面的實現類交給執行緒池的

execute 方法,作為他的一個引數,比如:

Executor e=Executors.newSingleThreadExecutor();          

e.execute(new Runnable(){ //匿名內部類     public  void run(){  

//需要執行的任務

}

});

執行緒池的實現原理

15592734-906e3b7531f86546

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

15592734-63c4da8fd45b2ec9

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

繼續往下看 runWorker

15592734-452d84508df56606

//  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 和異常作為引數,留給需要的子類實現




小編整理了一些java進階學習資料和麵試題,需要資料的請加JAVA高階學習Q群:664389243 這是小編建立的java高階學習交流群,加群一起交流學習深造。群裡也有小編整理的2019年最新最全的java高階學習資料!

相關文章