【搞定面試官】談談你對JDK中Executor的理解?

## 前言 隨着當今處理器計算能力愈發強大,可用的核心數量愈來愈多,各個應用對其實現更高吞吐量的需求的不斷增加,多線程 API 變得很是流行。在此背景下,Java自JDK1.5 提供了本身的多線程框架,稱爲 [Executor 框架](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html). ## 1. Executor 框架是什麼? ### 1.1 簡介 Java Doc中是這麼描述的 > An object that executes submitted [`Runnable`](https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html) 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. An `Executor` 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` 接口來實現多線程時,爲何須要線程池? 若是咱們不採用線程池,爲每個請求都建立一個線程的話: 1. **管理線程的生命週期開銷很是高**。管理這些線程的生命週期會明顯增長 CPU 的執行時間,會消耗大量計算資源。 2. **線程間上下文切換形成大量資源浪費**。 3. **程序穩定性會受到影響**。咱們知道,建立線程的數量存在一個限制,這個限制將隨着平臺的不一樣而不一樣,而且受多個因素制約,包括jvm的啓動參數、Thread構造函數中請求的棧大小,以及底層操做的限制等。若是超過了這個限制,那麼極可能拋出OutOfMemoryError異常,這對於運行中的應用來講是很是危險的。 全部的這些因素都會致使系統吞吐量降低。線程池經過保持一些存活線程並重用這些線程來克服這個問題。當提交到線程池中的任務多於線程池最大任務數時,那些多餘的任務將被放到一個`隊列`中。 一旦正在執行的線程有空閒了,它們會從隊列中取下一個任務來執行。JDK 中的 Executors中, 此任務隊列是沒有長度限制的。 ### 1.2 實現 咱們先來看一下Executor的實現關係。 ![file](https://img2018.cnblogs.com/blog/1110433/201911/1110433-20191130233616000-1595380007.jpg) 仍是蠻好理解的,正如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 只有一個線程。它用於以順序方式的形式執行任務。若是此線程在執行任務時因異常而掛掉,則會建立一個新線程來替換此線程,後續任務將在新線程中執行。 ```java ExecutorService executorService = Executors.newSingleThreadExecutor() ``` ### 2.2 FixedThreadPool(n) 顧名思義,它是一個擁有固定數量線程的線程池。提交給 Executor 的任務由固定的 `n` 個線程執行,若是有更多的任務,它們存儲在 `LinkedBlockingQueue` 裏。這個數字 `n` 一般跟底層處理器支持的線程總數有關。 ```java ExecutorService executorService = Executors.newFixedThreadPool(4); ``` ### 2.3 CachedThreadPool 該線程池主要用於執行大量短時間並行任務的場景。與固定線程池不一樣,此線程池的線程數不受限制。若是全部的線程都在忙於執行任務而且又有新的任務到來了,這個線程池將建立一個新的線程並將其提交到 Executor。只要其中一個線程變爲空閒,它就會執行新的任務。 若是一個線程有 60 秒的時間都是空閒的,它們將被結束生命週期並從緩存中刪除。 可是,若是管理得不合理,或者任務不是很短的,則線程池將包含大量的活動線程。這可能致使資源紊亂並所以致使性能降低。 ```java ExecutorService executorService = Executors.newCachedThreadPool(); ``` ### 2.4 ScheduledExecutor 當咱們有一個須要按期運行的任務或者咱們但願延遲某個任務時,就會使用此類型的 executor。 ```java ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1); ``` 可使用 `scheduleAtFixedRate` 或 `scheduleWithFixedDelay` 在 `ScheduledExecutor` 中按期的執行任務。 ```java 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)。 應用方式: ```java Future 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 中執行它: ```java public class Task implements Callable { 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 將此異常返回給調用者的能力很是重要,由於它有助於調用者知道任務執行的狀態。 如今讓咱們來執行一下這個任務: ```java public class ExecutorExample { public static void main(String[] args) { Task task = new Task("World"); ExecutorService executorService = Executors.newFixedThreadPool(4); Future 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` 並查看其輸出: ```bash Hello World! ``` 最後,咱們調用 `executorService` 對象上的 shutdown 來終止全部線程並將資源返回給 OS。 `shutdown()` 方法等待 Executor 完成當前提交的任務。 可是,若是要求是當即關閉 Executor 而不等待,那麼咱們可使用 `shutdownNow()` 方法。 任何待執行的任務都將結果返回到 `java.util.List` 對象中。 咱們也能夠經過實現 `Runnable` 接口來建立一樣的任務: ```java public class Task implements Runnable{ private String message; public Task(String message) { this.message = message; } public void run() { System.out.println("Hello " + message + "!"); } } ``` 當咱們實現 Runnable 時,這裏有一些重要的變化。 1. 沒法從 `run()` 方法獲得任務執行的結果。 所以,咱們直接在這裏打印。 2. `run()` 方法不可拋出任何已受檢的異常。 **Notes:如何合理配置線程池的大小** 通常須要根據任務的類型來配置線程池大小: 若是是**CPU密集型**任務,就須要儘可能壓榨CPU,參考值能夠設爲 NCPU+1 若是是**IO密集型**任務,參考值能夠設置爲2*NCPU 固然,這只是一個參考值,具體的設置還須要根據實際狀況進行調整,好比能夠先將線程池大小設置爲參考值,再觀察任務運行狀況和系統負載、資源利用率來進行適當調整。 您的點贊與支持是做者寫做的最大動力!
相關文章
相關標籤/搜索