介紹
隨着當今處理器中可用的核心數量的增加, 隨着對實現更高吞吐量的需求的不斷增長,多線程 API 變得非常流行。 Java 提供了自己的多線程框架,稱為 Executor 框架.
Executor 框架包含一組用於有效管理工作線程的組件。Executor API 通過 Executors
將任務的執行與要執行的實際任務解耦。 這是 生產者-消費者 模式的一種實現。
java.util.concurrent.Executors
提供了用於創建工作線程的線程池
的工廠方法。
為了使用 Executor 框架,我們需要創建一個線程池並提交任務給它以供執行。Executor 框架的工作是調度和執行已提交的任務並從線程池中拿到返回的結果。
浮現於腦海中的一個基本的問題是,當我們創建 java.lang.Thread
的對象或調用實現了 Runnable
/Callable
接口來達到的程序的並行性時,為什么需要線程池?
答案來源於兩個基本面:
- 為新任務創建新的線程會存在額外的線程創建以及銷毀的開銷。管理這些線程的生命周期會明顯增加 CPU 的執行時間。
- 不進行任何限制地為每個進程創建線程會導致創建大量線程。這些線程會占用大量內存並引起資源的浪費。當一個線程利用完 CPU 的時間片后另一個線程即將利用CPU的時間片時,CPU 會花費大量的時間來切換線程的上下文。
所有的這些因素都會導致系統的吞吐量下降。線程池通過保持線程一直存活並重用這些線程來克服這個問題。當提交到線程池中的任務多於正在執行的線程時,那些多余的任務將被放到隊列
中。 一旦執行任務的線程有空閑的了,它們會從隊列中取下一個任務來執行。對於 JDK 提供的現成的 executors 此任務隊列基本是無界的。
現在我們已經了解了 executors 是什么, 讓我們來看看不同類型的 executors。
此線程池 executor 只有一個線程。它用於以順序方式的形式執行任務。如果此線程在執行任務時因異常而掛掉,則會創建一個新線程來替換此線程,后續任務將在新線程中執行。
ExecutorService executorService = Executors.newSingleThreadExecutor()
顧名思義,它是一個擁有固定數量線程的線程池。提交給 executor 的任務由固定的 n
個線程執行,如果有更多的任務,它們存儲在 LinkedBlockingQueue
里。這個數字 n
通常跟底層處理器支持的線程總數有關。
ExecutorService executorService = Executors.newFixedThreadPool(4);
該線程池主要用於執行大量短期並行任務的場景。與固定線程池不同,此線程池的線程數不受限制。如果所有的線程都在忙於執行任務並且又有新的任務到來了,這個線程池將創建一個新的線程並將其提交到 executor。只要其中一個線程變為空閑,它就會執行新的任務。 如果一個線程有 60 秒的時間都是空閑的,它們將被結束生命周期並從緩存中刪除。
但是,如果管理得不合理,或者任務不是很短的,則線程池將包含大量的活動線程。這可能導致資源紊亂並因此導致性能下降。
ExecutorService executorService = Executors.newCachedThreadPool();
當我們有一個需要定期運行的任務或者我們希望延遲某個任務時,就會使用此類型的 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
:只有在當前任務完成后才會啟動延遲倒計時。
可以使用 executor 返回的 java.util.concurrent.Future
對象訪問提交給 executor 的任務的結果。 Future 可以被認為是 executor 對調用者的響應。
Future<String> result = executorService.submit(callableTask);
如上所述,提交給 executor 的任務是異步的,即程序不會等待當前任務執行完成,而是直接進入下一步。相反,每當任務執行完成時,executor 在此 Future
對象中設置它。
調用者可以繼續執行主程序,當需要提交任務的結果時,他可以在這個 Future
對象上調用.get()
方法來獲取。如果任務完成,結果將立即返回給調用者,否則調用者將被阻塞,直到 executor 完成此操作的執行並計算出結果。
如果調用者不能無限期地等待任務執行的結果,那么這個等待時間也可以設置為定時地。可以通過 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
時中斷任務執行。
我們現在將創建一個任務並嘗試在 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,因為這個 demo
是在四核處理器上開發的。如果正在執行的任務執行大量 I/O 操作或花費較長時間等待外部資源,則線程數可能超過處理器的核心數。
我們實例化了 Task
類,並將它提交給 executors 執行。 結果由 Future
對象返回,然后我們在屏幕上打印。
讓我們運行 ExecutorExample
並查看其輸出:
Hello World!
正如所料,任務追加了問候語 Hello
並通過 Future
object 返回結果。
最后,我們調用 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()
方法不可拋出任何已受檢的異常。
隨着處理器時鍾速度難以提高,多線程正變得越來越主流。 但是,由於涉及復雜性,處理每個線程的生命周期非常困難。
在本文中,我們展示了一個高效而簡單的多線程框架,即 Executor Framework,並解釋了它的不同組件。 我們還看了一下在 executor 中創建提交和執行任務的不同示例。
與往常一樣,此示例的代碼可以在 GitHub上找到。