簡介
作為Java 8 Concurrency API改進而引入,本文是CompletableFuture類的功能和用例的介紹。同時在Java 9 也有對CompletableFuture有一些改進,之後再進入講解。
Future計算
Future非同步計算很難操作,通常我們希望將任何計算邏輯視為一系列步驟。但是在非同步計算的情況下,表示為回撥的方法往往分散在程式碼中或者深深地巢狀在彼此內部。但是當我們需要處理其中一個步驟中可能發生的錯誤時,情況可能會變得更復雜。
Futrue介面是Java 5中作為非同步計算而新增的,但它沒有任何方法去進行計算組合或者處理可能出現的錯誤。
在Java 8中,引入了CompletableFuture類。與Future介面一起,它還實現了CompletionStage介面。此介面定義了可與其他Future組合成非同步計算契約。
CompletableFuture同時是一個組合和一個框架,具有大約50種不同的構成,結合,執行非同步計算步驟和處理錯誤。
如此龐大的API可能會令人難以招架,下文將調一些重要的做重點介紹。
使用CompletableFuture作為Future實現
首先,CompletableFuture類實現Future介面,因此你可以將其用作Future實現,但需要額外的完成實現邏輯。
例如,你可以使用無構參建構函式建立此類的例項,然後使用complete
方法完成。消費者可以使用get方法來阻塞當前執行緒,直到get()
結果。
在下面的示例中,我們有一個建立CompletableFuture例項的方法,然後在另一個執行緒中計算並立即返回Future。
計算完成後,該方法通過將結果提供給完整方法來完成Future:
public Future<String> calculateAsync() throws InterruptedException {
CompletableFuture<String> completableFuture
= new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() -> {
Thread.sleep(500);
completableFuture.complete("Hello");
return null;
});
return completableFuture;
}
複製程式碼
為了分離計算,我們使用了Executor API ,這種建立和完成CompletableFuture的方法可以與任何併發包(包括原始執行緒)一起使用。
請注意,該calculateAsync
方法返回一個Future
例項。
我們只是呼叫方法,接收Future例項並在我們準備阻塞結果時呼叫它的get方法。
另請注意,get方法丟擲一些已檢查的異常,即ExecutionException(封裝計算期間發生的異常)和InterruptedException(表示執行方法的執行緒被中斷的異常):
Future<String> completableFuture = calculateAsync();
// ...
String result = completableFuture.get();
assertEquals("Hello", result);
複製程式碼
如果你已經知道計算的結果,也可以用變成同步的方式來返回結果。
Future<String> completableFuture =
CompletableFuture.completedFuture("Hello");
// ...
String result = completableFuture.get();
assertEquals("Hello", result);
複製程式碼
作為在某些場景中,你可能希望取消Future任務的執行。
假設我們沒有找到結果並決定完全取消非同步執行任務。這可以通過Future的取消方法完成。此方法mayInterruptIfRunning
,但在CompletableFuture的情況下,它沒有任何效果,因為中斷不用於控制CompletableFuture的處理。
這是非同步方法的修改版本:
public Future<String> calculateAsyncWithCancellation() throws InterruptedException {
CompletableFuture<String> completableFuture = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() -> {
Thread.sleep(500);
completableFuture.cancel(false);
return null;
});
return completableFuture;
}
複製程式碼
當我們使用Future.get()方法阻塞結果時,cancel()
表示取消執行,它將丟擲CancellationException:
Future<String> future = calculateAsyncWithCancellation();
future.get(); // CancellationException
複製程式碼
API介紹
static方法說明
上面的程式碼很簡單,下面介紹幾個 static 方法,它們使用任務來例項化一個 CompletableFuture 例項。
CompletableFuture.runAsync(Runnable runnable);
CompletableFuture.runAsync(Runnable runnable, Executor executor);
CompletableFuture.supplyAsync(Supplier<U> supplier);
CompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor)
複製程式碼
- runAsync 方法接收的是 Runnable 的例項,但是它沒有返回值
- supplyAsync 方法是JDK8函式式介面,無引數,會返回一個結果
- 這兩個方法是 executor 的升級,表示讓任務在指定的執行緒池中執行,不指定的話,通常任務是在 ForkJoinPool.commonPool() 執行緒池中執行的。
supplyAsync()使用
靜態方法runAsync
和supplyAsync
允許我們相應地從Runnable和Supplier功能型別中建立CompletableFuture例項。
該Runnable的介面是線上程使用舊的介面,它不允許返回值。
Supplier介面是一個不具有引數,並返回引數化型別的一個值的單個方法的通用功能介面。
這允許將Supplier的例項作為lambda表示式提供,該表示式執行計算並返回結果:
CompletableFuture<String> future
= CompletableFuture.supplyAsync(() -> "Hello");
// ...
assertEquals("Hello", future.get());
複製程式碼
thenRun()使用
在兩個任務任務A,任務B中,如果既不需要任務A的值也不想在任務B中引用,那麼你可以將Runnable lambda 傳遞給thenRun()
方法。在下面的示例中,在呼叫future.get()方法之後,我們只需在控制檯中列印一行:
模板
CompletableFuture.runAsync(() -> {}).thenRun(() -> {});
CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {});
複製程式碼
- 第一行用的是
thenRun(Runnable runnable)
,任務 A 執行完執行 B,並且 B 不需要 A 的結果。 - 第二行用的是
thenRun(Runnable runnable)
,任務 A 執行完執行 B,會返回resultA
,但是 B 不需要 A 的結果。
實戰
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<Void> future = completableFuture
.thenRun(() -> System.out.println("Computation finished."));
future.get();
複製程式碼
thenAccept()使用
在兩個任務任務A,任務B中,如果你不需要在Future中有返回值,則可以用 thenAccept
方法接收將計算結果傳遞給它。最後的future.get()呼叫返回Void型別的例項。
模板
CompletableFuture.runAsync(() -> {}).thenAccept(resultA -> {});
CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {});
複製程式碼
- 第一行中,
runAsync
不會有返回值,第二個方法thenAccept
,接收到的resultA值為null,同時任務B也不會有返回結果 - 第二行中,
supplyAsync
有返回值,同時任務B不會有返回結果。
實戰
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<Void> future = completableFuture
.thenAccept(s -> System.out.println("Computation returned: " + s));
future.get();
複製程式碼
thenApply()使用
在兩個任務任務A,任務B中,任務B想要任務A計算的結果,可以用thenApply
方法來接受一個函式例項,用它來處理結果,並返回一個Future函式的返回值:
模板
CompletableFuture.runAsync(() -> {}).thenApply(resultA -> "resultB");
CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB");
複製程式碼
- 第二行用的是 thenApply(Function fn),任務 A 執行完執行 B,B 需要 A 的結果,同時任務 B 有返回值。
實戰
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future = completableFuture
.thenApply(s -> s + " World");
assertEquals("Hello World", future.get());
複製程式碼
當然,多個任務的情況下,如果任務 B 後面還有任務 C,往下繼續呼叫 .thenXxx() 即可。
thenCompose()使用
接下來會有一個很有趣的設計模式;
CompletableFuture API 的最佳場景是能夠在一系列計算步驟中組合CompletableFuture例項。
這種組合結果本身就是CompletableFuture,允許進一步再續組合。這種方法在函式式語言中無處不在,通常被稱為monadic設計模式
。
簡單說,Monad就是一種設計模式,表示將一個運算過程,通過函式拆解成互相連線的多個步驟。你只要提供下一步運算所需的函式,整個運算就會自動進行下去。
在下面的示例中,我們使用thenCompose方法按順序組合兩個Futures。
請注意,此方法採用返回CompletableFuture例項的函式。該函式的引數是先前計算步驟的結果。這允許我們在下一個CompletableFuture的lambda中使用這個值:
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello")
.thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World"));
assertEquals("Hello World", completableFuture.get());
複製程式碼
該thenCompose方法連同thenApply一樣實現了結果的合併計算。但是他們的內部形式是不一樣的,它們與Java 8中可用的Stream和Optional類的map和flatMap方法是有著類似的設計思路在裡面的。
兩個方法都接收一個CompletableFuture並將其應用於計算結果,但thenCompose(flatMap)方法接收一個函式,該函式返回相同型別的另一個CompletableFuture物件。此功能結構允許將這些類的例項繼續進行組合計算。
thenCombine()
取兩個任務的結果
如果要執行兩個獨立的任務,並對其結果執行某些操作,可以用Future的thenCombine方法:
模板
CompletableFuture<String> cfA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture<String> cfB = CompletableFuture.supplyAsync(() -> "resultB");
cfA.thenAcceptBoth(cfB, (resultA, resultB) -> {});
cfA.thenCombine(cfB, (resultA, resultB) -> "result A + B");
複製程式碼
實戰
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello")
.thenCombine(CompletableFuture.supplyAsync(
() -> " World"), (s1, s2) -> s1 + s2));
assertEquals("Hello World", completableFuture.get());
複製程式碼
更簡單的情況是,當你想要使用兩個Future結果時,但不需要將任何結果值進行返回時,可以用thenAcceptBoth
,它表示後續的處理不需要返回值,而 thenCombine 表示需要返回值:
CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
.thenAcceptBoth(CompletableFuture.supplyAsync(() -> " World"),
(s1, s2) -> System.out.println(s1 + s2));
複製程式碼
thenApply()和thenCompose()之間的區別
在前面的部分中,我們展示了關於thenApply()和thenCompose()的示例。這兩個API都是使用的CompletableFuture呼叫,但這兩個API的使用是不同的。
thenApply()
此方法用於處理先前呼叫的結果。但是,要記住的一個關鍵點是返回型別是轉換泛型中的型別,是同一個CompletableFuture。
因此,當我們想要轉換CompletableFuture 呼叫的結果時,效果是這樣的 :
CompletableFuture<Integer> finalResult = compute().thenApply(s-> s + 1);
複製程式碼
thenCompose()
該thenCompose()方法類似於thenApply()在都返回一個新的計算結果。但是,thenCompose()使用前一個Future作為引數。它會直接使結果變新的Future,而不是我們在thenApply()中到的巢狀Future,而是用來連線兩個CompletableFuture,是生成一個新的CompletableFuture:
CompletableFuture<Integer> computeAnother(Integer i){
return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture<Integer> finalResult = compute().thenCompose(this::computeAnother);
複製程式碼
因此,如果想要繼續巢狀連結CompletableFuture 方法,那麼最好使用thenCompose()。
並行執行多個任務
當我們需要並行執行多個任務時,我們通常希望等待所有它們執行,然後處理它們的組合結果。
該CompletableFuture.allOf
靜態方法允許等待所有的完成任務:
API
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs){...}
複製程式碼
實戰
CompletableFuture<String> future1
= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2
= CompletableFuture.supplyAsync(() -> "Beautiful");
CompletableFuture<String> future3
= CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<Void> combinedFuture
= CompletableFuture.allOf(future1, future2, future3);
// ...
combinedFuture.get();
assertTrue(future1.isDone());
assertTrue(future2.isDone());
assertTrue(future3.isDone());
複製程式碼
請注意,CompletableFuture.allOf()的返回型別是CompletableFuture 。這種方法的侷限性在於它不會返回所有任務的綜合結果。相反,你必須手動從Futures獲取結果。幸運的是,CompletableFuture.join()方法和Java 8 Streams API可以解決:
String combined = Stream.of(future1, future2, future3)
.map(CompletableFuture::join)
.collect(Collectors.joining(" "));
assertEquals("Hello Beautiful World", combined);
複製程式碼
CompletableFuture 提供了 join() 方法,它的功能和 get() 方法是一樣的,都是阻塞獲取值,它們的區別在於 join() 丟擲的是 unchecked Exception。這使得它可以在Stream.map()方法中用作方法引用。
異常處理
說到這裡,我們順便來說下 CompletableFuture 的異常處理。這裡我們要介紹兩個方法:
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn);
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
複製程式碼
看下程式碼
CompletableFuture.supplyAsync(() -> "resultA")
.thenApply(resultA -> resultA + " resultB")
.thenApply(resultB -> resultB + " resultC")
.thenApply(resultC -> resultC + " resultD");
複製程式碼
上面的程式碼中,任務 A、B、C、D 依次執行,如果任務 A 丟擲異常(當然上面的程式碼不會丟擲異常),那麼後面的任務都得不到執行。如果任務 C 丟擲異常,那麼任務 D 得不到執行。
那麼我們怎麼處理異常呢?看下面的程式碼,我們在任務 A 中丟擲異常,並對其進行處理:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException();
})
.exceptionally(ex -> "errorResultA")
.thenApply(resultA -> resultA + " resultB")
.thenApply(resultB -> resultB + " resultC")
.thenApply(resultC -> resultC + " resultD");
System.out.println(future.join());
複製程式碼
上面的程式碼中,任務 A 丟擲異常,然後通過.exceptionally()
方法處理了異常,並返回新的結果,這個新的結果將傳遞給任務 B。所以最終的輸出結果是:
errorResultA resultB resultC resultD
String name = null;
// ...
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> {
if (name == null) {
throw new RuntimeException("Computation error!");
}
return "Hello, " + name;
})}).handle((s, t) -> s != null ? s : "Hello, Stranger!");
assertEquals("Hello, Stranger!", completableFuture.get());
複製程式碼
當然,它們也可以都為 null,因為如果它作用的那個 CompletableFuture 例項沒有返回值的時候,s 就是 null。
Async字尾方法
CompletableFuture類中的API的大多數方法都有兩個帶有Async字尾的附加修飾。這些方法表示用於非同步執行緒。
沒有Async字尾的方法使用呼叫執行緒執行下一個執行執行緒階段。不帶Async方法使用ForkJoinPool.commonPool()執行緒池的fork / join實現運算任務。帶有Async方法使用傳遞式的Executor任務去執行。
下面附帶一個案例,可以看到有thenApplyAsync方法。在程式內部,執行緒被包裝到ForkJoinTask例項中。這樣可以進一步並行化你的計算並更有效地使用系統資源。
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future = completableFuture
.thenApplyAsync(s -> s + " World");
assertEquals("Hello World", future.get());
複製程式碼
JDK 9 CompletableFuture API
在Java 9中, CompletableFuture API通過以下更改得到了進一步增強:
- 新工廠方法增加了
- 支援延遲和超時
- 改進了對子類化的支援。
引入了新的例項API:
- Executor defaultExecutor()
- CompletableFuture newIncompleteFuture()
- CompletableFuture copy()
- CompletionStage minimalCompletionStage()
- CompletableFuture completeAsync(Supplier<? extends T> supplier, Executor executor)
- CompletableFuture completeAsync(Supplier<? extends T> supplier)
- CompletableFuture orTimeout(long timeout, TimeUnit unit)
- CompletableFuture completeOnTimeout(T value, long timeout, TimeUnit unit)
還有一些靜態實用方法:
- Executor delayedExecutor(long delay, TimeUnit unit, Executor executor)
- Executor delayedExecutor(long delay, TimeUnit unit)
- CompletionStage completedStage(U value)
- CompletionStage failedStage(Throwable ex)
- CompletableFuture failedFuture(Throwable ex)
最後,為了解決超時問題,Java 9又引入了兩個新功能:
- orTimeout()
- completeOnTimeout()
結論
在本文中,我們描述了CompletableFuture類的方法和典型用例。