前言
隨著當今處理器計算能力愈發強大,可用的核心數量越來越多,各個應用對其實現更高吞吐量的需求的不斷增長,多執行緒 API 變得非常流行。在此背景下,Java自JDK1.5 提供了自己的多執行緒框架,稱為 Executor 框架.
1. Executor 框架是什麼?
1.1 簡介
Java Doc中是這麼描述的
An object that executes submitted
Runnable
tasks. This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc. AnExecutor
is normally used instead of explicitly creating threads.執行提交的Runnable任務的物件。這個介面提供了一種將任務提交與如何執行每個任務的機制,包括執行緒的詳細資訊使用、排程等。通常使用Executor而不是顯式地建立執行緒。
我們可以這麼理解:Executor就是一個執行緒池框架,在開發中如果需要建立執行緒可優先考慮使用Executor,無論你需要多執行緒還是單執行緒,Executor為你提供了很多其他功能,包括執行緒狀態,生命週期的管理。
Executor 位於java.util.concurrent.Executors
,提供了用於建立工作執行緒的執行緒池的工廠方法。它包含一組用於有效管理工作執行緒的元件。Executor API 通過 Executors
將任務的執行與要執行的實際任務解耦。 這是 生產者-消費者
模式的一種實現。
浮現於腦海中的一個基本的問題是,當我們建立 java.lang.Thread
物件或呼叫實現了 Runnable
/Callable
介面來實現多執行緒時,為什麼需要執行緒池?
如果我們不採用執行緒池,為每一個請求都建立一個執行緒的話:
- 管理執行緒的生命週期開銷非常高。管理這些執行緒的生命週期會明顯增加 CPU 的執行時間,會消耗大量計算資源。
- 執行緒間上下文切換造成大量資源浪費。
- 程式穩定性會受到影響。我們知道,建立執行緒的數量存在一個限制,這個限制將隨著平臺的不同而不同,並且受多個因素制約,包括jvm的啟動引數、Thread建構函式中請求的棧大小,以及底層操作的限制等。如果超過了這個限制,那麼很可能丟擲OutOfMemoryError異常,這對於執行中的應用來說是非常危險的。
所有的這些因素都會導致系統吞吐量下降。執行緒池通過保持一些存活執行緒並重用這些執行緒來克服這個問題。當提交到執行緒池中的任務多於執行緒池最大任務數時,那些多餘的任務將被放到一個佇列
中。 一旦正在執行的執行緒有空閒了,它們會從佇列中取下一個任務來執行。JDK 中的 Executors中, 此任務佇列是沒有長度限制的。
1.2 實現
我們先來看一下Executor的實現關係。
還是蠻好理解的,正如Java優秀框架的一貫設計思路,頂級介面-次級介面-虛擬實現類-實現類。
**Executor:**執行者,java執行緒池框架的最上層父介面,地位類似於spring的BeanFactry、集合框架的Collection介面,在Executor這個介面中只有一個execute方法,該方法的作用是向執行緒池提交任務並執行。
**ExecutorService:**該介面繼承自Executor介面,新增了shutdown、shutdownAll、submit、invokeAll等一系列對執行緒的操作方法,該介面比較重要,在使用執行緒池框架的時候,經常用到該介面。
**AbstractExecutorService:**這是一個抽象類,實現ExecuotrService介面,
**ThreadPoolExecutor:**這是Java執行緒池最核心的一個類,該類繼承自AbstractExecutorService,主要功能是建立執行緒池,給任務分配執行緒資源,執行任務。
ScheduledExecutorSerivce 和 ScheduledThreadPoolExecutor 提供了另一種執行緒池:延遲執行和週期性執行的執行緒池。
**Executors:**這是一個靜態工廠類,該類定義了一系列靜態工廠方法,通過這些工廠方法可以返回各種不同的執行緒池。
2. Executors 的型別
現在我們已經瞭解了 Executors 是什麼, 讓我們來看看不同型別的 Executors。
2.1 SingleThreadExecutor
此執行緒池 Executor 只有一個執行緒。它用於以順序方式的形式執行任務。如果此執行緒在執行任務時因異常而掛掉,則會建立一個新執行緒來替換此執行緒,後續任務將在新執行緒中執行。
ExecutorService executorService = Executors.newSingleThreadExecutor()
複製程式碼
2.2 FixedThreadPool(n)
顧名思義,它是一個擁有固定數量執行緒的執行緒池。提交給 Executor 的任務由固定的 n
個執行緒執行,如果有更多的任務,它們儲存在 LinkedBlockingQueue
裡。這個數字 n
通常跟底層處理器支援的執行緒總數有關。
ExecutorService executorService = Executors.newFixedThreadPool(4);
複製程式碼
2.3 CachedThreadPool
該執行緒池主要用於執行大量短期並行任務的場景。與固定執行緒池不同,此執行緒池的執行緒數不受限制。如果所有的執行緒都在忙於執行任務並且又有新的任務到來了,這個執行緒池將建立一個新的執行緒並將其提交到 Executor。只要其中一個執行緒變為空閒,它就會執行新的任務。 如果一個執行緒有 60 秒的時間都是空閒的,它們將被結束生命週期並從快取中刪除。
但是,如果管理得不合理,或者任務不是很短的,則執行緒池將包含大量的活動執行緒。這可能導致資源紊亂並因此導致效能下降。
ExecutorService executorService = Executors.newCachedThreadPool();
複製程式碼
2.4 ScheduledExecutor
當我們有一個需要定期執行的任務或者我們希望延遲某個任務時,就會使用此型別的 executor。
ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);
複製程式碼
可以使用 scheduleAtFixedRate
或 scheduleWithFixedDelay
在 ScheduledExecutor
中定期的執行任務。
scheduledExecService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
scheduledExecService.scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)
複製程式碼
這兩種方法的主要區別在於它們對連續執行定期任務之間的延遲的應答。
scheduleAtFixedRate
:無論前一個任務何時結束,都以固定間隔執行任務。
scheduleWithFixedDelay
:只有在當前任務完成後才會啟動延遲倒數計時。
3. 對 Future 物件的理解
由於提交給Executor 的任務是非同步的,需要有一個物件來接收Executor 的處理結果,這個物件就是java.util.concurrent.Future
(類似於JS中的Promise)。
應用方式:
Future<String> result = executorService.submit(callableTask);
複製程式碼
呼叫者可以繼續執行主程式,當需要提交任務的結果時,他可以在這個 Future
物件上呼叫.get()
方法來獲取。如果任務完成,結果將立即返回給呼叫者,否則呼叫者將被阻塞,直到 Executor 完成此操作的執行並計算出結果。(瞭解JS的童鞋此處可以和Promise的then()相類比)。
如果呼叫者不能無限期地等待任務執行的結果,那麼這個等待時間也可以設定為定時地。可以通過 Future.get(long timeout,TimeUnit unit)
方法實現,如果在規定的時間範圍內沒有返回結果,則丟擲 TimeoutException
。呼叫者可以處理此異常並繼續執行該程式。
如果在執行任務時出現異常,則對 get 方法的呼叫將丟擲一個ExecutionException
。
對於 Future.get()
方法返回的結果,一個重要的事情是,只有提交的任務實現了java.util.concurrent.Callable
介面時才返回 Future
。如果任務實現了Runnable
介面,那麼一旦任務完成,對 .get()
方法的呼叫將返回 null
。
另一點是 Future.cancel(boolean mayInterruptIfRunning)
方法。此方法用於取消已提交任務的執行。如果任務已在執行,則 Executor 將嘗試在mayInterruptIfRunning
標誌為 true
時中斷任務執行。
4. Example: 建立和執行一個簡單的 Executor
我們現在將建立一個任務並嘗試在 fixed pool Executor 中執行它:
public class Task implements Callable<String> {
private String message;
public Task(String message) {
this.message = message;
}
@Override
public String call() throws Exception {
return "Hello " + message + "!";
}
}
複製程式碼
Task
類實現 Callable
介面並有一個 String
型別作為返回值的方法。 這個方法也可以丟擲 Exception
。這種向 Executor 丟擲異常的能力以及 Executor 將此異常返回給呼叫者的能力非常重要,因為它有助於呼叫者知道任務執行的狀態。
現在讓我們來執行一下這個任務:
public class ExecutorExample {
public static void main(String[] args) {
Task task = new Task("World");
ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<String> result = executorService.submit(task);
try {
System.out.println(result.get());
} catch (InterruptedException | ExecutionException e) {
System.out.println("Error occured while executing the submitted task");
e.printStackTrace();
}
executorService.shutdown();
}
}
複製程式碼
我們建立了一個具有4個執行緒數的 FixedThreadPool
Executors,並例項化了 Task
類,並將它提交給 Executors 執行。 結果由 Future
物件返回,然後我們在螢幕上列印。
讓我們執行 ExecutorExample
並檢視其輸出:
Hello World!
複製程式碼
最後,我們呼叫 executorService
物件上的 shutdown 來終止所有執行緒並將資源返回給 OS。
shutdown()
方法等待 Executor 完成當前提交的任務。 但是,如果要求是立即關閉 Executor 而不等待,那麼我們可以使用 shutdownNow()
方法。
任何待執行的任務都將結果返回到 java.util.List
物件中。
我們也可以通過實現 Runnable
介面來建立同樣的任務:
public class Task implements Runnable{
private String message;
public Task(String message) {
this.message = message;
}
public void run() {
System.out.println("Hello " + message + "!");
}
}
複製程式碼
當我們實現 Runnable 時,這裡有一些重要的變化。
- 無法從
run()
方法得到任務執行的結果。 因此,我們直接在這裡列印。 run()
方法不可丟擲任何已受檢的異常。
Notes:如何合理配置執行緒池的大小
一般需要根據任務的型別來配置執行緒池大小:
如果是CPU密集型任務,就需要儘量壓榨CPU,參考值可以設為 NCPU+1 如果是IO密集型任務,參考值可以設定為2*NCPU 當然,這只是一個參考值,具體的設定還需要根據實際情況進行調整,比如可以先將執行緒池大小設定為參考值,再觀察任務執行情況和系統負載、資源利用率來進行適當調整。
您的點贊與關注是對作者寫作的最大的支援,謝謝!
歡迎各位關注個人公眾號(關注後Java程式設計師必備Spring Mybatics以及其他框架的入門和拔高視訊哦)