原文:Java 8 CompletableFutures Part I
- 作者:Bill Bejeck
- 譯者:noONE
譯者前言
JDK1.5就增加了Future介面,但是介面使用不是很能滿足非同步開發的需求,使用起來不是那麼友好。所以出現了很多第三方封裝的
Future
,Guava中就提供了一個更好的 ListenableFuture 類,Netty中則提供了一個自己的Future
。所以,Java8中的CompletableFuture
可以說是解決Future
了一些痛點,可以優雅得進行組合式非同步程式設計,同時也更加契合函數語言程式設計。
Java8已經發布了很長一段時間,其中新增了一個很棒的併發控制工具,就是CompletableFuture類。CompletableFuture
實現了Future介面,並且它可以顯式地設定值,更有意思的是我們可以進行鏈式處理,並且支援依賴行為,這些行為由CompletableFuture
完成所觸發。CompletableFuture
類似於Guava中的 ListenableFuture 類。它們兩個提供了類似的功能,本文不會再對它們進行對比。我已經在之前的文章中介紹過ListenableFutrue
。雖然對於ListenableFutrue
的介紹有點過時,但是絕大數的知識仍然適用。CompletableFuture
的文件已經非常全面了,但是缺少如何使用它們的具體示例 。本文意在通過單元測試中的一系列的簡單示例來展示如何使用CompletableFuture
。最初我想在一篇文章中介紹完CompleteableFuture
,但是資訊太多了,分成三部分似乎更好一些:
- 建立/組合任務以及為它們增加監聽器。
- 處理錯誤以及錯誤恢復。
- 取消或者強制完成。
CompletableFuture 入門
在開始使用CompletableFuture
之前, 我們需要了解一些背景知識。CompletableFuture
實現了 CompletionStage 介面。javadoc中簡明地介紹了CompletionStage
:
一個可能的非同步計算的階段,當另外一個CompletionStage 完成時,它會執行一個操作或者計算一個值。一個階段的完成取決於它本身結算的結果,同時也可能反過來觸發其他依賴階段。
CompletionStage 的全部文件的內容很多,所以,我們在這裡總結幾個關鍵點:
-
計算可以由 Future ,Consumer 或者 Runnable 介面中的 apply,accept 或者 run等方法表示。
-
計算的執行主要有以下
a. 預設執行(可能呼叫執行緒)
b. 使用預設的
CompletionStage
的非同步執行提供者非同步執行。這些方法名使用someActionAsync這種格式表示。c. 使用 Executor 提供者非同步執行。這些方法同樣也是someActionAsync這種格式,但是會增加一個
Executor
引數。
接下來,我會在本文中直接引用CompletableFuture
和 CompletionStage
。
建立一個CompleteableFuture
建立一個CompleteableFuture
很簡單,但是不是很清晰。最簡單的方法就是使用CompleteableFuture.completedFuture
方法,該方法返回一個新的且完結的CompleteableFuture
:
@Test
public void test_completed_future() throws Exception {
String expectedValue = "the expected value";
CompletableFuture<String> alreadyCompleted = CompletableFuture.completedFuture(expectedValue);
assertThat(alreadyCompleted.get(), is(expectedValue));
}
複製程式碼
這樣看起來有點乏味,稍後,我們就會看到如何建立一個已經完成的CompleteableFuture
會派上用場。
現在,讓我們看一下如何建立一個表示非同步任務的CompleteableFuture
:
private static ExecutorService service = Executors.newCachedThreadPool();
@Test
public void test_run_async() throws Exception {
CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> System.out.println("running async task"), service);
//utility testing method
pauseSeconds(1);
assertThat(runAsync.isDone(), is(true));
}
@Test
public void test_supply_async() throws Exception {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(simulatedTask(1, "Final Result"), service);
assertThat(completableFuture.get(), is("Final Result"));
}
複製程式碼
在第一個方法中,我們看到了runAsync
任務,在第二個方法中,則是supplyAsync
的示例。這可能是顯而易見的,然而使用runAsync
還是使用supplyAsync
,這取決於任務是否有返回值。在這兩個例子中,我們都提供了一個自定義的Executor
,它作為一個非同步執行提供者。當使用supplyAsync
方法時,我個人認為使用 Callable 而不是一個Supplier
似乎更自然一些。因為它們都是函式式介面,Callable
與非同步任務的關係更緊密一些,並且它還可以丟擲受檢異常,而Supplier
則不會(儘管我們可以通過少量的程式碼讓Supplier
丟擲受檢異常)。
增加監聽器
現在,我們可以建立CompleteableFuture
物件去執行非同步任務,讓我們開始學習如何去“監聽”任務的完成,並且執行隨後的一些動作。這裡重點提一下,當增加對 CompletionStage
物件的追隨時,之前的任務需要徹底成功,後續的任務和階段才能執行。本文會介紹介紹一些處理失敗任務的方法,而在CompleteableFuture
中鏈式處理錯誤的方案會在後續的文章中介紹。
@Test
public void test_then_run_async() throws Exception {
Map<String,String> cache = new HashMap<>();
cache.put("key","value");
CompletableFuture<String> taskUsingCache = CompletableFuture.supplyAsync(simulatedTask(1,cache.get("key")),service);
CompletableFuture<Void> cleanUp = taskUsingCache.thenRunAsync(cache::clear,service);
cleanUp.get();
String theValue = taskUsingCache.get();
assertThat(cache.isEmpty(),is(true));
assertThat(theValue,is("value"));
}
複製程式碼
這個例子主要展示在第一個CompletableFuture
成功結束後,執行一個清理的任務。 在之前的例子中,當最初的任務成功結束後,我們使用Runnable
任務執行。我們也可以定義一個後續任務,它可以直接獲取之前任務的成功結果。
@Test
public void test_accept_result() throws Exception {
CompletableFuture<String> task = CompletableFuture.supplyAsync(simulatedTask(1, "add when done"), service);
CompletableFuture<Void> acceptingTask = task.thenAccept(results::add);
pauseSeconds(2);
assertThat(acceptingTask.isDone(), is(true));
assertThat(results.size(), is(1));
assertThat(results.contains("add when done"), is(true));
}
複製程式碼
這是一個使用Accept 方法的例子,該方法會獲取CompletableFuture
的結果,然後將結果傳給一個 Consumer
物件。在Java 8中, Consumer
例項是沒有返回值的 ,如果想得到執行的副作用,需要把結果放到一個列表中。
組合與構成任務
除了增加監聽器去執行後續任務或者接受CompletableFuture
的成功結果,我們還可以組合或者構成任務。
構成任務
構成意味著獲取一個成功的CompletableFuture
結果作為輸入,通過 一個Function 返回另外一個 CompletableFuture
。下面是一個使用CompletableFuture.thenComposeAsync
的例子:
@Test
public void test_then_compose() throws Exception {
Function<Integer,Supplier<List<Integer>>> getFirstTenMultiples = num ->
()->Stream.iterate(num, i -> i + num).limit(10).collect(Collectors.toList());
Supplier<List<Integer>> multiplesSupplier = getFirstTenMultiples.apply(13);
//Original CompletionStage
CompletableFuture<List<Integer>> getMultiples = CompletableFuture.supplyAsync(multiplesSupplier, service);
//Function that takes input from orignal CompletionStage
Function<List<Integer>, CompletableFuture<Integer>> sumNumbers = multiples ->
CompletableFuture.supplyAsync(() -> multiples.stream().mapToInt(Integer::intValue).sum());
//The final CompletableFuture composed of previous two.
CompletableFuture<Integer> summedMultiples = getMultiples.thenComposeAsync(sumNumbers, service);
assertThat(summedMultiples.get(), is(715));
}
複製程式碼
在這個列子中,第一個CompletionStage
提供了一個列表,該列表包含10個數字,每個數字都乘以13。這個提供的Function
獲取這些結果,並且建立另外一個CompletionStage
,它將對列表中的數字求和。
組合任務
組合任務的完成是通過獲取兩個成功的CompletionStages
,並且從中獲取BiFunction型別的引數,進而產出另外的結果。以下是一個非常簡單的例子用來說明從組合的CompletionStages
中獲取結果。
@Test
public void test_then_combine_async() throws Exception {
CompletableFuture<String> firstTask = CompletableFuture.supplyAsync(simulatedTask(3, "combine all"), service);
CompletableFuture<String> secondTask = CompletableFuture.supplyAsync(simulatedTask(2, "task results"), service);
CompletableFuture<String> combined = firstTask.thenCombineAsync(secondTask, (f, s) -> f + " " + s, service);
assertThat(combined.get(), is("combine all task results"));
}
複製程式碼
這個例子展示瞭如何組合兩個非同步任務的CompletionStage
,然而,我們也可以組合已經完成的CompletableFuture
的非同步任務。 組合一個已知的需要計算的值,也是一種很好的處理方式:
@Test
public void test_then_combine_with_one_supplied_value() throws Exception {
CompletableFuture<String> asyncComputedValue = CompletableFuture.supplyAsync(simulatedTask(2, "calculated value"), service);
CompletableFuture<String> knowValueToCombine = CompletableFuture.completedFuture("known value");
BinaryOperator<String> calcResults = (f, s) -> "taking a " + f + " then adding a " + s;
CompletableFuture<String> combined = asyncComputedValue.thenCombine(knowValueToCombine, calcResults);
assertThat(combined.get(), is("taking a calculated value then adding a known value"));
}
複製程式碼
最後,是一個使用CompletableFuture.runAfterbothAsync
的例子
@Test
public void test_run_after_both() throws Exception {
CompletableFuture<Void> run1 = CompletableFuture.runAsync(() -> {
pauseSeconds(2);
results.add("first task");
}, service);
CompletableFuture<Void> run2 = CompletableFuture.runAsync(() -> {
pauseSeconds(3);
results.add("second task");
}, service);
CompletableFuture<Void> finisher = run1.runAfterBothAsync(run2,() -> results. add(results.get(0)+ "&"+results.get(1)),service);
pauseSeconds(4);
assertThat(finisher.isDone(),is(true));
assertThat(results.get(2),is("first task&second task"));
}
複製程式碼
監聽第一個結束的任務
在之前所有的例子中,所有的結果需要等待所有的CompletionStage
結束,然而,需求並不總是這樣的。我們可能需要獲取第一個完成的任務的結果。下面的例子展示使用Consumer
接受第一個完成的結果:
@Test
public void test_accept_either_async_nested_finishes_first() throws Exception {
CompletableFuture<String> callingCompletable = CompletableFuture.supplyAsync(simulatedTask(2, "calling"), service);
CompletableFuture<String> nestedCompletable = CompletableFuture.supplyAsync(simulatedTask(1, "nested"), service);
CompletableFuture<Void> collector = callingCompletable.acceptEither(nestedCompletable, results::add);
pauseSeconds(2);
assertThat(collector.isDone(), is(true));
assertThat(results.size(), is(1));
assertThat(results.contains("nested"), is(true));
}
複製程式碼
類似功能的CompletableFuture.runAfterEither
@Test
public void test_run_after_either() throws Exception {
CompletableFuture<Void> run1 = CompletableFuture.runAsync(() -> {
pauseSeconds(2);
results.add("should be first");
}, service);
CompletableFuture<Void> run2 = CompletableFuture.runAsync(() -> {
pauseSeconds(3);
results.add("should be second");
}, service);
CompletableFuture<Void> finisher = run1.runAfterEitherAsync(run2,() -> results.add(results.get(0).toUpperCase()),service);
pauseSeconds(4);
assertThat(finisher.isDone(),is(true));
assertThat(results.get(1),is("SHOULD BE FIRST"));
}
複製程式碼
多重組合
到目前為止,所有的組合/構成的例子都只有兩個CompletableFuture
物件。這裡是有意為之,為了讓例子儘量的簡單明瞭。我們可以組合任意數量的CompletionStage
。請注意,下面例子僅僅是為了說明而已!
@Test
public void test_several_stage_combinations() throws Exception {
Function<String,CompletableFuture<String>> upperCaseFunction = s -> CompletableFuture.completedFuture(s.toUpperCase());
CompletableFuture<String> stage1 = CompletableFuture.completedFuture("the quick ");
CompletableFuture<String> stage2 = CompletableFuture.completedFuture("brown fox ");
CompletableFuture<String> stage3 = stage1.thenCombine(stage2,(s1,s2) -> s1+s2);
CompletableFuture<String> stage4 = stage3.thenCompose(upperCaseFunction);
CompletableFuture<String> stage5 = CompletableFuture.supplyAsync(simulatedTask(2,"jumped over"));
CompletableFuture<String> stage6 = stage4.thenCombineAsync(stage5,(s1,s2)-> s1+s2,service);
CompletableFuture<String> stage6_sub_1_slow = CompletableFuture.supplyAsync(simulatedTask(4,"fell into"));
CompletableFuture<String> stage7 = stage6.applyToEitherAsync(stage6_sub_1_slow,String::toUpperCase,service);
CompletableFuture<String> stage8 = CompletableFuture.supplyAsync(simulatedTask(3," the lazy dog"),service);
CompletableFuture<String> finalStage = stage7.thenCombineAsync(stage8,(s1,s2)-> s1+s2,service);
assertThat(finalStage.get(),is("THE QUICK BROWN FOX JUMPED OVER the lazy dog"));
}
複製程式碼
需要注意的是,組合CompletionStage的時候並不保證順序。在這些單元測試中,提供了一個時間去模擬任務以確保完成順序。
小結
本文主要是使用CompletableFuture
類的第一部分。在後續文章中,將主要介紹錯誤處理及恢復,強制完成或取消。
資源
關注我: