Java中ExecutorService與CompletableFuture指南

banq發表於2024-03-22

在本教程中,我們將探討兩個重要的 Java 類,用於處理需要併發執行的任務:ExecutorService和CompletableFuture。我們將學習它們的功能以及如何有效地使用它們,並且我們將瞭解它們之間的主要區別。

ExecutorService
ExecutorService是 Java 的java.util.concurrent包中的一個功能強大的介面,它簡化了需要併發執行的任務的管理。它抽象了執行緒建立、管理和排程的複雜性,使我們能夠專注於需要完成的實際工作。

ExecutorService提供了像submit()和execute()這樣的方法 來提交我們想要併發執行的任務。然後,這些任務將排隊並分配給執行緒池中的可用執行緒。如果任務返回結果,我們可以使用Future物件來檢索它們。但是,  在 Future上使用get()等方法檢索結果可能會阻塞呼叫執行緒,直到任務完成。

ExecutorService 專注於管理執行緒池和併發執行任務。它提供了建立具有不同配置的執行緒池的方法,例如固定大小、快取和排程。

讓我們看一個使用ExecutorService建立和維護三個執行緒的示例:

ExecutorService executor = Executors.newFixedThreadPool(3);
Future<Integer> future = executor.submit(() -> {
    <font>// Task execution logic<i>
    return 42;
});

newFixedThreadPool (3)方法呼叫建立一個包含三個執行緒的執行緒池,確保併發執行的任務不會超過三個。然後使用submit ()方法提交一個任務線上程池中執行,返回一個代表計算結果的Future物件。

CompletableFuture
CompletableFuture是在 Java 8 中引入的。它專注於組合非同步操作並以更具宣告性的方式處理其最終結果。 CompletableFuture 充當儲存非同步操作的 最終結果的容器。它可能不會立即得到結果,但它提供了方法來定義當結果可用時要做什麼。

與ExecutorService 檢索結果可能會阻塞執行緒 不同 , CompletableFuture 以非阻塞方式執行。

CompletableFuture為組合非同步操作提供了更高階別的抽象。它側重於定義工作流程並處理非同步任務的最終結果。

下面是一個使用SupplyAsync() 啟動返回字串的非同步任務的示例:

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    return 42;
});

在此示例中,supplyAsync()啟動一個非同步任務,返回結果 42。


雖然ExecutorService和CompletableFuture都處理 Java 中的非同步程式設計,但它們的用途不同。

連結非同步任務
ExecutorService和CompletableFuture都提供了連結非同步任務的機制,但它們採用了不同的方法。

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 <font>"Result based on Task 1: " + result;
    } catch (InterruptedException | ExecutionException e) {
       
// Handle exception<i>
    }
    return null;
});
executor.shutdown();

在此示例中,第二個任務取決於第一個任務的結果。但是,ExecutorService不提供內建連結,因此我們需要在提交第二個任務之前,透過使用get()等待第一個任務完成(這會阻塞執行緒)來顯式管理依賴關係。

CompletableFuture
另一方面,CompletableFuture提供了一種更加簡化和更具表現力的方式來連結非同步任務。它使用thenApply()等內建方法簡化了任務鏈。這些方法允許您定義一系列非同步任務,其中一個任務的輸出成為下一個任務的輸入。

這是使用CompletableFuture 的等效示例:

CompletableFuture<Integer> firstTask = CompletableFuture.supplyAsync(() -> {
    return 42;
});
CompletableFuture<String> secondTask = firstTask.thenApply(result -> {
    return <font>"Result based on Task 1: " + result;
});

在此示例中,thenApply()方法用於定義第二個任務,該任務取決於第一個任務的結果。當我們使用thenApply()將任務連結到CompletableFuture時,主執行緒不會等待第一個任務完成後再繼續。它繼續執行我們程式碼的其他部分。

錯誤處理
使用ExecutorService時,錯誤可以透過兩種方式體現:

  • 提交的任務中丟擲的異常:當我們嘗試 在返回的Future物件上使用get()等方法檢索結果時,這些異常會傳播回主執行緒​​。如果處理不當,這可能會導致意外行為。
  • 執行緒池管理期間的未經檢查的異常:如果線上程池建立或關閉期間發生未經檢查的異常,通常是從 ExecutorService 方法本身丟擲的。我們需要在程式碼中捕獲並處理這些異常。

讓我們看一個示例,突出顯示潛在的問題:

ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future = executor.submit(() -> {
    if (someCondition) {
        throw new RuntimeException(<font>"Something went wrong!");
    }
    return
"Success";
});
try {
    String result = future.get();
    System.out.println(
"Result: " + result);
} catch (InterruptedException | ExecutionException e) {
   
// Handle exception<i>
} finally {
    executor.shutdown();
}

在此示例中,如果滿足特定條件,則提交的任務將引發異常。但是,我們需要在future.get()周圍使用try-catch塊來捕獲任務丟擲的異常或使用get()檢索期間丟擲的異常。這種方法對於跨多個任務管理錯誤可能會變得乏味。

相比之下,CompletableFuture提供了一種更強大的錯誤處理方法,包括使用 excepting()等方法 以及在連結方法本身內處理異常。這些方法允許我們定義如何在非同步工作流程的不同階段處理錯誤,而不需要顯式的try-catch塊。

這是使用CompletableFuture進行錯誤處理的等效示例:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (someCondition) {
        throw new RuntimeException(<font>"Something went wrong!");
    }
    return
"Success";
})
.exceptionally(ex -> {
    System.err.println(
"Error in task: " + ex.getMessage());
    return
"Error occurred"; // Can optionally return a default value<i>
});
future.thenAccept(result -> System.out.println(
"Result: " + result));

在此示例中,非同步任務引發異常,並且錯誤在Exceptionly()回撥中被捕獲和處理。它在發生異常時提供預設值(“發生錯誤”)。

CompletableFuture提供exceptedly()、whenComplete()、在連結方法中處理

超時管理
超時管理在非同步程式設計中至關重要,可以確保任務在指定的時間範圍內完成。讓我們探討一下ExecutorService和CompletableFuture如何以不同的方式處理超時。

ExecutorService不提供內建超時功能。為了實現超時,我們需要使用Future物件並可能中斷超過截止日期的任務。這種方法涉及手動協調:

ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future = executor.submit(() -> {
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        System.err.println(<font>"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.<i>
} catch (Exception e) {
   
// Handle exception<i>
} finally {
    executor.shutdown();
}

在此示例中,我們向 ExecutorService 提交一個任務,並在使用get()方法檢索結果時指定兩秒的超時。如果任務完成時間超過指定的超時時間,則會引發TimeoutException 。這種方法可能容易出錯,需要小心處理。

需要注意的是,雖然超時機制中斷了對任務結果的等待,但任務本身將繼續在後臺執行,直到完成或被中斷。例如,要中斷 ExecutorService 中執行的任務,我們需要使用Future.cancel(true)方法。

總之:ExecutorService 使用Future.get(timeout)的手動協調和潛在的中斷

在 Java 9 中,CompletableFuture提供了一種更簡化的超時方法,例如completeOnTimeout()等方法。如果原始任務未在指定的超時時間內完成,則completeOnTimeout()方法將以指定的值完成CompletableFuture 。

讓我們看一個示例來說明completeOnTimeout()的工作原理:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        <font>// Handle exception<i>
    }
    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!”值。

 

相關文章