ExecutorService 與 CompletableFuture 指南
一、簡介
在本教程中,我們將探討兩個重要的 Java 類,用於處理需要並發運行的任務: ExecutorService
和CompletableFuture
。我們將學習它們的功能以及如何有效地使用它們,並且我們將了解它們之間的主要差異。
ExecutorService
概述
ExecutorService
是 Java 的java.util.concurrent
套件中的一個強大的接口,它簡化了需要並發運行的任務的管理。它抽象化了線程創建、管理和調度的複雜性,使我們能夠專注於需要完成的實際工作。
ExecutorService
提供了像submit()
和execute()
這樣的方法來提交我們想要並發運行的任務。然後,這些任務將排隊並分配給執行緒池中的可用執行緒。如果任務傳回結果,我們可以使用Future
物件來檢索它們。但是,在Future
上使用get()
等方法檢索結果可能會阻塞呼叫線程,直到任務完成。
CompletableFuture
概述
CompletableFuture
是在 Java 8 中引入的。它專注於組合非同步操作並以更具聲明性的方式處理其最終結果。 CompletableFuture
可作為保存非同步操作的最終結果的容器。它可能不會立即得到結果,但它提供了方法來定義當結果可用時要做什麼。
與ExecutorService,
擷取結果可能會阻塞執行緒不同, CompletableFuture
以非阻塞方式運作。
四、工作重點和職責
雖然ExecutorService
和CompletableFuture
都處理 Java 中的非同步編程,但它們的用途不同。讓我們探討一下他們各自的重點和責任。
4.1. ExecutorService
ExecutorService
專注於管理線程池和並發執行任務。它提供了創建具有不同配置的線程池的方法,例如固定大小、快取和調度。
讓我們來看一個使用ExecutorService
建立和維護三個執行緒的範例:
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<Integer> future = executor.submit(() -> {
// Task execution logic
return 42;
});
newFixedThreadPool(3)
方法呼叫建立一個包含三個執行緒的執行緒池,確保並發執行的任務不會超過三個。然後使用submit()
方法提交一個任務在執行緒池中執行,並傳回一個代表計算結果的Future
物件。
4.2. CompletableFuture
相較之下, CompletableFuture
為組合非同步操作提供了更高層級的抽象化。它側重於定義工作流程並處理非同步任務的最終結果。
下面是一個使用supplyAsync()
啟動返回字串的非同步任務的範例:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 42;
});
在此範例中, supplyAsync()
啟動一個非同步任務,傳回結果 42。
5.連結異步任務
ExecutorService
和CompletableFuture
都提供了連結非同步任務的機制,但它們採用了不同的方法。
5.1. ExecutorService
在ExecutorService
中,我們通常會提交任務來執行,然後使用這些任務傳回的Future
物件來處理依賴關係並連結後續任務。然而,這涉及到阻塞並等待每個任務完成後再繼續下一個任務,這可能導致處理非同步工作流程的效率低下。
考慮這樣的情況:我們向ExecutorService
提交兩個任務,然後使用Future
物件將它們連結在一起:
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> firstTask = executor.submit(() -> {
return 42;
});
Future<String> secondTask = executor.submit(() -> {
try {
Integer result = firstTask.get();
return "Result based on Task 1: " + result;
} catch (InterruptedException | ExecutionException e) {
// Handle exception
}
return null;
});
executor.shutdown();
在此範例中,第二個任務取決於第一個任務的結果。但是, ExecutorService
不提供內建鏈接,因此我們需要在提交第二個任務之前,透過使用get()
等待第一個任務完成(這會阻塞線程)來明確管理依賴關係。
5.2. CompletableFuture
另一方面, CompletableFuture
提供了一種更簡化和更具表現力的方式來連結非同步任務。它使用thenApply()
等內建方法簡化了任務鏈。這些方法可讓您定義一系列非同步任務,其中一個任務的輸出成為下一個任務的輸入。
這是使用CompletableFuture
等效範例:
CompletableFuture<Integer> firstTask = CompletableFuture.supplyAsync(() -> {
return 42;
});
CompletableFuture<String> secondTask = firstTask.thenApply(result -> {
return "Result based on Task 1: " + result;
});
在此範例中, thenApply()
方法用於定義第二個任務,該任務取決於第一個任務的結果。當我們使用thenApply()
將任務連結到CompletableFuture
時,主執行緒不會等待第一個任務完成後再繼續。它繼續執行我們程式碼的其他部分。
6. 錯誤處理
在本節中,我們將研究ExecutorService
和CompletableFuture
如何管理錯誤和異常情況。
6.1. ExecutorService
使用ExecutorService
時,錯誤可以透過兩種方式體現:
- 提交的任務中拋出的異常:當我們嘗試在傳回的
Future
物件上使用get()
等方法檢索結果時,這些異常會傳回主執行緒。如果處理不當,這可能會導致意外行為。 - 在執行緒池管理期間的未經檢查的例外:如果在執行緒池建立或關閉期間發生未經檢查的異常,通常是從
ExecutorService
方法本身拋出的。我們需要在程式碼中捕獲並處理這些異常。
讓我們看一個範例,突出顯示潛在的問題:
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future = executor.submit(() -> {
if (someCondition) {
throw new RuntimeException("Something went wrong!");
}
return "Success";
});
try {
String result = future.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
// Handle exception
} finally {
executor.shutdown();
}
在此範例中,如果滿足特定條件,則提交的任務將引發異常。但是,我們需要在future.get()
周圍使用try-catch
區塊來捕獲任務拋出的異常或使用get()
檢索期間拋出的異常。這種方法對於跨多個任務管理錯誤可能會變得乏味。
6.2. CompletableFuture
相較之下, CompletableFuture
提供了一種更強大的錯誤處理方法,包括exceptionally()
等方法以及在連結方法本身內處理例外狀況。這些方法讓我們可以定義如何在非同步工作流程的不同階段處理錯誤,而不需要明確的try-catch
區塊。
這是使用CompletableFuture
進行錯誤處理的等效範例:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (someCondition) {
throw new RuntimeException("Something went wrong!");
}
return "Success";
})
.exceptionally(ex -> {
System.err.println("Error in task: " + ex.getMessage());
return "Error occurred"; // Can optionally return a default value
});
future.thenAccept(result -> System.out.println("Result: " + result));
在此範例中,非同步任務引發異常,並且錯誤在exceptionally()
回呼中被捕獲和處理。它在發生異常時提供預設值(“發生錯誤”)。
7. 逾時管理
超時管理在非同步程式設計中至關重要,可確保任務在指定的時間範圍內完成。讓我們探討一下ExecutorService
和CompletableFuture
如何以不同的方式處理逾時。
7.1. ExecutorService
ExecutorService
不提供內建逾時功能。為了實現逾時,我們需要使用Future
物件並可能中斷超過截止日期的任務。這種方法涉及手動協調:
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future = executor.submit(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.err.println("Error occured: " + e.getMessage());
}
return "Task completed";
});
try {
String result = future.get(2, TimeUnit.SECONDS);
System.out.println("Result: " + result);
} catch (TimeoutException e) {
System.err.println("Task execution timed out!");
future.cancel(true); // Manually interrupt the task.
} catch (Exception e) {
// Handle exception
} finally {
executor.shutdown();
}
在此範例中,我們向ExecutorService
提交一個任務,並在使用get()
方法擷取結果時指定兩秒的逾時。如果任務完成時間超過指定的逾時時間,則會引發TimeoutException
。這種方法可能容易出錯,需要小心處理。
需要注意的是,雖然超時機制中斷了對任務結果的等待,但任務本身將繼續在背景運行,直到完成或中斷。例如,要中斷ExecutorService
中執行的任務,我們需要使用Future.cancel(true)
方法。
7.2. CompletableFuture
在 Java 9 中, CompletableFuture
提供了一種更簡化的逾時方法,例如completeOnTimeout()
等方法。如果原始任務未在指定的逾時時間內完成,則completeOnTimeout()
方法將以指定的值完成CompletableFuture
。
讓我們來看一個範例來說明completeOnTimeout()
的工作原理:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// Handle exception
}
return "Task completed";
});
CompletableFuture<String> timeoutFuture = future.completeOnTimeout("Timed out!", 2, TimeUnit.SECONDS);
String result = timeoutFuture.join();
System.out.println("Result: " + result);
在此範例中, supplyAsync()
方法啟動一個非同步任務,該任務模擬長時間運行的操作,需要五秒鐘才能完成。但是,我們使用completeOnTimeout()
指定兩秒的超時。如果任務在兩秒內沒有完成, CompletableFuture
將自動完成,並顯示「Timed out!」值。
八、總結
下面的比較表總結了ExecutorService
和CompletableFuture
之間的主要差異:
特徵 | ExecutorService |
CompletableFuture |
---|---|---|
重點 | 執行緒池管理和任務執行 | 編寫非同步操作並處理最終結果 |
連結 | 與Future 物件手動協調 |
thenApply() 等內建方法 |
錯誤處理 | Future.get() 周圍的手動 try-catch 區塊 |
exceptionally() 、 whenComplete() 、在連結方法中處理 |
逾時管理 | 與Future.get(timeout) 的手動協調和潛在的中斷 |
內建方法,如completeOnTimeout() |
阻塞與非阻塞 | 阻塞(通常等待Future.get() 檢索結果) |
非阻塞(連結任務而不阻塞主執行緒) |
9. 結論
在本文中,我們探討了處理非同步任務的兩個基本類別: ExecutorService
和CompletableFuture
。 ExecutorService
簡化了執行緒池和並發任務執行的管理,而CompletableFuture
提供了更高層級的抽象來組合非同步操作並處理其結果。
我們還研究了它們的功能、差異以及它們如何處理錯誤處理、超時管理和非同步任務連結。
與往常一樣,範例的原始程式碼可在 GitHub 上取得。