深入淺出 Java 執行緒池

佔小狼發表於2016-08-19

前言

Java1.5後引入的Executor框架的最大優點是把任務的提交和執行解耦,只需把Task描述清楚,然後提交即可。至於這個Task是怎麼被執行的,被誰執行的,什麼時候執行的,就全部交給執行緒池管理。

小案例

廢話不多說,先來一個小小案例。

程式碼很簡單:初始化一個執行緒池,提交一個任務,主執行緒的future.get()會阻塞執行緒直到任務執行完成。

Executor框架成員

執行緒池實現框架中包含了一堆實現類,它們之間的關係如下,只有瞭解了各個類之間的關係,才能方便我們更好的理解執行緒池的實現。
paste_image
Paste_Image.png

從圖中可以看到Executor、ExecutorService、ScheduledExecutorService定義執行緒池介面,ThreadPoolExecutor和ScheduledThreadPoolExecutor是執行緒池的實現,前者是一個普通的執行緒池,後者一個定期排程的執行緒池,Executors是輔助工具,用以幫助我們快速定義執行緒池。

引數

在初始化執行緒池時,不同的應用場景中,對引數的選擇是很重要的,先來看看執行緒池的各個引數的含義:

1.workQueue:用來儲存等待執行的任務的阻塞佇列。

2.corePoolSize:執行緒池的基本大小。當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使有其它空閒的執行緒,直到執行緒數達到corePoolSize時就不再建立,這時會把提交的新任務放到阻塞佇列。如果呼叫了執行緒池的prestartAllCoreThreads()方法,執行緒池會提前建立並啟動所有基本執行緒。

3.maximumPoolSize:執行緒池允許建立的最大執行緒數。如果阻塞佇列滿了,並且已經建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。

4.threadFactory:建立執行緒的工廠。可以通過自定義執行緒工廠給每個執行緒設定有意義的名稱。如guava提供的ThreadFactoryBuilder。

5.rejectedExecutionHandler:飽和策略。當阻塞佇列滿了且沒有空閒的工作執行緒,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略在預設情況下是AbortPolicy,表示無法處理新任務時丟擲異常。不過,執行緒池提供了4種策略:

1、AbortPolicy:直接丟擲異常。
2、CallerRunsPolicy:只用呼叫者所在的執行緒來執行任務。
3、DiscardOldestPolicy:丟棄阻塞佇列中最近的一個任務,並執行當前任務。
4、DiscardPolicy:直接丟棄。

當然,也可以根據應用場景來實現RejectedExecutionHandler介面自定義飽和策略,如記錄日誌或持久化儲存不能處理的任務。

6.keepAliveTime:執行緒活動保持時間。指工作執行緒空閒後,繼續保持存活的時間。預設情況下,這個引數只有線上程數大於corePoolSize時才起作用。所以,如果任務很多,且每個任務的執行時間比較短,可以調大keepAliveTime,提高執行緒的利用率。

在初始化執行緒池時,對阻塞佇列的選擇也很重要,jdk中提供了以下幾個阻塞佇列:

  • ArrayBlockingQueue:基於陣列結構的有界阻塞佇列,按FIFO原則對元素進行排序。
  • LinkedBlockingQuene:基於連結串列結構的阻塞佇列,按FIFO排序元素,吞吐量通常要高於ArrayBlockingQuene。
  • SynchronousQuene:一個不儲存元素的阻塞佇列,每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQuene。
  • priorityBlockingQuene:具有優先順序的無界阻塞佇列。

Exectors

Exectors是java執行緒池的工廠類,通過它可以快速初始化一個符合業務需求的執行緒池,主要提供了以下幾種便捷的方式:

1.newFixedThreadPool:建立一個指定工作執行緒數的執行緒池,其中引數corePoolSize和maximumPoolSize相等,阻塞佇列基於LinkedBlockingQuene。

它是一個典型且優秀的執行緒池,它具有執行緒池提高程式效率和節省建立執行緒時所耗的開銷的優點。但是線上程池空閒時,即執行緒池中沒有可執行任務時,它不會釋放工作執行緒,還會佔用一定的系統資源。

2.newCachedThreadPool:建立一個可快取工作執行緒的執行緒池(工作執行緒預設存活時間1分鐘)。該執行緒池有以下特點:
1、工作執行緒數量幾乎沒有上線,因為maximumPoolSize為Integer.MAX_VALUE(2147483647)。
2、如果長時間沒有提交任務,且工作執行緒空閒了指定的時間,則該工作執行緒將自動終止。如果重新提交了任務,則執行緒池重新建立一個工作執行緒。

它在沒有任務執行時,會釋放工作執行緒,從而釋放工作執行緒所佔用的資源。但是,但當提交新任務時,又要建立新的工作執行緒,有一定的系統開銷。另外一定要注意控制任務的數量,否則由於大量執行緒同時執行,很有會造成系統癱瘓。

3.newSingleThreadExecutor:建立一個只有單一工作執行緒的執行緒池。如果這個工作執行緒異常結束,會有另一個取代它。唯一的工作執行緒可以保證任務的順序執行。

4.newScheduledThreadPool:建立一個可以在指定時間內週期性的執行任務的執行緒池。在實際業務中常用的場景是週期性的同步資料。

Future和Callable

如果提交的任務需要返回結果,必須實現Callable介面。在上述小案例中,我們向執行緒池提交了一個實現Callable介面的任務,並通過Future的get方法,獲取到返回值。第一次看這個程式碼,估計會有疑惑,等理解原理之後,就可以大徹大悟了。

在實際業務場景中,Callable和Future是成對出現的,Callable負責產生結果,Future負責獲取結果。
Callable介面類似於Runnable,不過Runnable沒有返回值。

Callable任務被執行,除了可以返回執行結果之外,如果任務發生異常,這個異常也可以被Future獲取,即Future可以拿到非同步執行任務各種結果。

通過深入分析FutureTask(Future的實現類)來看看案例中的邏輯是如何實現的:
Callable任務執行完成後返回一個FutureTask物件,FutureTask實現了Runnable和 Future介面。

1.通過submit提交任務後,任務被封裝成一個FutureTask物件。

2.因為FutureTask實現了Runable介面,可以通過執行緒池的execute執行,這個過程後續會說明。

3.主執行緒執行FutureTask.get方法,導致主執行緒阻塞,看看是如何實現的:

如果FutureTask的狀態state小於等於COMPLETING,awaitDone方法最終會通過 LockSupport.park阻塞主執行緒。

4.執行緒池中的工作執行緒執行FutureTask的run方法,程式碼如下:

5.FutureTask的run方法中,如果任務執行成功,執行set(v),設定正常返回值,否則執行setException(e)設定異常,其中喚醒主執行緒的奧祕就在這裡中,一起來看下是如何實現的。

1、 把正常返回值或執行期間捕獲的異常賦值給outcome。
2、設定FutureTask的狀態為NORMAL或EXCEPTIONAL。
3、通過LockSupport.unpark(t)喚醒主執行緒。

任務提交

向執行緒池提交任務有兩種:

1.execute():用於提交不需要返回值的任務,這個方式無法判斷任務是否執行成功。
executor.execute(runnableTask);

2.submit():用於提交需要返回值的任務。執行緒池會返回一個Future物件,通過這個物件可以判斷任務是否執行成功。

大家注意到沒有,我對兩個方法的引數用了不同的變數,是因為方便大家的理解,execute提交的任務需要實現Runnable介面,而submit提交的任務需要實現Callable介面。

實現原理

接下去一起進入執行緒池內部的實現細節。

ctl這個變數,第一眼看上去,完全不知所云,其實它是用來描述執行緒池的狀態:整數的高3位表示執行緒池的執行狀態、低29位用來描述工作執行緒(後面會講到)數量,最多表示2^29 – 1,當然實際應用中不可能建立這麼多執行緒。不要問我為什麼要這麼設計,我猜,實在猜不出來,大概位運算效率高吧。其中,執行緒池的執行狀態有如下幾種:

1.RUNNING : 接受新任務並且處理已經進入阻塞佇列的任務。

2.SHUTDOWN : 不接受新任務,但是處理已經進入阻塞佇列的任務。

3.STOP : 不接受新任務,不處理已經進入阻塞佇列的任務並且中斷正在執行的任務。

4.TIDYING : 所有的任務都已經終止,workerCount為0, 執行緒轉化為TIDYING狀態並且呼叫terminated鉤子函式 。

5.TERMINATED: terminated鉤子函式已經執行完成。
當向執行緒池提交一個任務時,執行緒池是如何處理這個任務?先看示意圖和程式碼實現:

Paste_Image1

Paste_Image.png

1.如果當前執行的執行緒數少於corePoolSize,則建立新的工作執行緒處理任務,否則進入步驟2。

2.如果執行緒池處於執行狀態,則把任務放入BlockingQueue中,如果可用工作執行緒為0時,則建立新的工作執行緒,處理BlockingQueue的任務。

3.如果無法將任務加入到BlockingQueue,則建立新的執行緒處理任務,前提是目前執行的執行緒數小於maximumPoolSize,否則進入步驟4 任務被拒絕。

由於工作執行緒時存放在HashSet中的,所以在訪問工作執行緒的時候,需要進行加鎖操作。

以前我有一個疑問:為什麼執行緒池中的一個執行緒在生命週期內可以連續執行多個任務?按理說執行緒執行完之後,理應被回收銷燬。

執行緒池建立執行緒時,會將執行緒封裝成工作執行緒Worker:

從上面可以看出,工作執行緒Worker繼承了同步器AQS,對同步器AQS不瞭解的可以看看深入淺出java同步器,同時還實現Runable介面,為什麼要這麼設計?因為線上程池建立工作執行緒worker成功後,直接呼叫work.start()方法啟動該執行緒(即在worker例項中初始化的執行緒),並在runWorker方法中傳遞了自身例項,接下去讓我們看看執行緒池的核心方法runWorker:

從上述實現可以看出,

1.工作執行緒開始執行任務之前,先釋放鎖(設定state為0),表示允許中斷。

2.如果當前任務為空,則通過getTask從阻塞佇列中獲取任務。如果執行緒池狀態或配置引數改變,導致getTask返回null,或者其它內部錯誤丟擲異常,會觸發processWorkerExit方法。

3.在工作執行緒執行任務之前,需要獲取鎖。其實我沒明白這裡為什麼要獲取鎖,註釋上是說為了防止在任務執行期間,其它執行緒中斷,瞭解的同學可以留言告知,不慎感激。

4.每個任務執行時都會觸發beforeExecute操作,使用者可以自定義任務執行前的操作,如果beforeExecute丟擲異常,就不會執行任務了。

5.假設beforeExecute和task都執行完成,會觸發afterExecute操作,這個方法也可以自定義使用者操作。

工作執行緒在執行完當前任務後,通過while迴圈,從阻塞佇列中獲取任務。

1.workQueue.take()方法,如果阻塞佇列中沒有任務,當前工作執行緒在阻塞佇列的條件變數上等待掛起。

2.workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)方法,如果在keepAliveTime時間內,阻塞佇列還是沒有任務,返回null。

總結

合理的利用執行緒池,可以給我們帶來以下好處:

1.降低資源消耗。
2.提高響應速度。
3.提高執行緒的可管理性。

執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配、調優和監控。

ThreadPoolExecutor是執行緒池框架的一個核心類,通過對原始碼的分析,可以知道其對資源進行了複用,並非無限制的建立執行緒,可以有效的減少執行緒建立和切換的開銷。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

深入淺出 Java 執行緒池

相關文章