程式設計老司機帶你玩轉 CompletableFuture 非同步程式設計
阿新 • • 發佈:2020-03-09
本文從例項出發,介紹 `CompletableFuture` 基本用法。不過講的再多,不如親自上手練習一下。所以建議各位小夥伴看完,上機練習一把,快速掌握 `CompletableFuture`。
> 個人博文地址:https://sourl.cn/s5MbCm
**全文摘要:**
- `Future` VS `CompletableFuture`
- `CompletableFuture` 基本用法
## 0x00. 前言
一些業務場景我們需要使用多執行緒非同步執行任務,加快任務執行速度。 Java 提供 `Runnable` `Future` 兩個介面用來實現非同步任務邏輯。
雖然 `Future` 可以獲取任務執行結果,但是獲取方式十方不變。我們不得不使用`Future#get` 阻塞呼叫執行緒,或者使用輪詢方式判斷 `Future#isDone` 任務是否結束,再獲取結果。
這兩種處理方式都不是很優雅,JDK8 之前併發類庫沒有提供相關的非同步回撥實現方式。沒辦法,我們只好藉助第三方類庫,如 `Guava`,擴充套件 `Future`,增加支援回撥功能。相關程式碼如下:
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080830077-1051350991.jpg)
雖然這種方式增強了 Java 非同步程式設計能力,但是還是無法解決多個非同步任務需要相互依賴的場景。
舉一個生活上的例子,假如我們需要出去旅遊,需要完成三個任務:
- 任務一:訂購航班
- 任務二:訂購酒店
- 任務三:訂購租車服務
很顯然任務一和任務二沒有相關性,可以單獨執行。但是任務三必須等待任務一與任務二結束之後,才能訂購租車服務。
為了使任務三時執行時能獲取到任務一與任務二執行結果,我們還需要藉助 `CountDownLatch` 。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080830337-1137184891.jpg)
## 0x01. CompletableFuture
JDK8 之後,Java 新增一個功能十分強大的類:`CompletableFuture`。單獨使用這個類就可以輕鬆的完成上面的需求:
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080830551-292151003.jpg)
> 大家可以先不用管 `CompletableFuture` 相關 `API`,下面將會具體講解。
對比 `Future`,`CompletableFuture` 優點在於:
- 不需要手工分配執行緒,JDK 自動分配
- 程式碼語義清晰,非同步任務鏈式呼叫
- 支援編排非同步任務
怎麼樣,是不是功能很強大?接下來抓穩了,小黑哥要發車了。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080830724-1604495668.gif)
### 1.1 方法一覽
首先來通過 IDE 檢視下這個類提供的方法:
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080831082-1756910796.jpg)
稍微數一下,這個類總共有 50 多個方法,我的天。。。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080831448-721706481.jpg)
不過也不要怕,小黑哥幫你們歸納好了,跟著小黑哥的節奏,帶你們掌握 `CompletableFuture`。
> 若圖片不清晰,可以關注『程式通事』,回覆:『233』,獲取該思維導圖
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080831612-327784688.jpg)
### 1.2 建立 CompletableFuture 例項
建立 `CompletableFuture` 物件例項我們可以使用如下幾個方法:
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080831833-1895207835.jpg)
第一個方法建立一個具有預設結果的 `CompletableFuture`,這個沒啥好講。我們重點講述下下面四個非同步方法。
前兩個方法 `runAsync` 不支援返回值,而 `supplyAsync`可以支援返回結果。
這個兩個方法預設將會使用公共的 `ForkJoinPool` 執行緒池執行,這個執行緒池預設執行緒數是 **CPU** 的核數。
> 可以設定 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 來設定 ForkJoinPool 執行緒池的執行緒數
使用共享執行緒池將會有個弊端,一旦有任務被阻塞,將會造成其他任務沒機會執行。所以**強烈**建議使用後兩個方法,根據任務型別不同,主動建立執行緒池,進行資源隔離,避免互相干擾。
### 1.3 設定任務結果
`CompletableFuture` 提供以下方法,可以主動設定任務結果。
```java
boolean complete(T value)
boolean completeExceptionally(Throwable ex)
```
第一個方法,主動設定 `CompletableFuture` 任務執行結果,若返回 `true`,表示設定成功。如果返回 `false`,設定失敗,這是因為任務已經執行結束,已經有了執行結果。
示例程式碼如下:
```java
// 執行非同步任務
CompletableFuture cf = CompletableFuture.supplyAsync(() -> {
System.out.println("cf 任務執行開始");
sleep(10, TimeUnit.SECONDS);
System.out.println("cf 任務執行結束");
return "樓下小黑哥";
});
//
Executors.newSingleThreadScheduledExecutor().execute(() -> {
sleep(5, TimeUnit.SECONDS);
System.out.println("主動設定 cf 任務結果");
// 設定任務結果,由於 cf 任務未執行結束,結果返回 true
cf.complete("程式通事");
});
// 由於 cf 未執行結束,將會被阻塞。5 秒後,另外一個執行緒主動設定任務結果
System.out.println("get:" + cf.get());
// 等待 cf 任務執行結束
sleep(10, TimeUnit.SECONDS);
// 由於已經設定任務結果,cf 執行結束任務結果將會被拋棄
System.out.println("get:" + cf.get());
/***
* cf 任務執行開始
* 主動設定 cf 任務結果
* get:程式通事
* cf 任務執行結束
* get:程式通事
*/
```
這裡需要注意一點,一旦 `complete` 設定成功,`CompletableFuture` 返回結果就不會被更改,即使後續 `CompletableFuture` 任務執行結束。
第二個方法,給 `CompletableFuture` 設定異常物件。若設定成功,如果呼叫 `get` 等方法獲取結果,將會拋錯。
示例程式碼如下:
```java
// 執行非同步任務
CompletableFuture cf = CompletableFuture.supplyAsync(() -> {
System.out.println("cf 任務執行開始");
sleep(10, TimeUnit.SECONDS);
System.out.println("cf 任務執行結束");
return "樓下小黑哥";
});
//
Executors.newSingleThreadScheduledExecutor().execute(() -> {
sleep(5, TimeUnit.SECONDS);
System.out.println("主動設定 cf 異常");
// 設定任務結果,由於 cf 任務未執行結束,結果返回 true
cf.completeExceptionally(new RuntimeException("啊,掛了"));
});
// 由於 cf 未執行結束,前 5 秒將會被阻塞。後續程式丟擲異常,結束
System.out.println("get:" + cf.get());
/***
* cf 任務執行開始
* 主動設定 cf 異常
* java.util.concurrent.ExecutionException: java.lang.RuntimeException: 啊,掛了
* ......
*/
```
### 1.4 CompletionStage
`CompletableFuture` 分別實現兩個介面 `Future`與 `CompletionStage`。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080831993-1537839515.jpg)
`Future` 介面大家都比較熟悉,這裡主要講講 `CompletionStage`。
`CompletableFuture` 大部分方法來自`CompletionStage` 介面,正是因為這個介面,`CompletableFuture`才有如從強大功能。
想要理解 `CompletionStage` 介面,我們需要先了解任務的時序關係的。我們可以將任務時序關係分為以下幾種:
- 序列執行關係
- 並行執行關係
- AND 匯聚關係
- OR 匯聚關係
### 1.5 序列執行關係
任務序列執行,下一個任務必須等待上一個任務完成才可以繼續執行。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080832143-1617974285.jpg)
`CompletionStage` 有四組介面可以描述序列這種關係,分別為:
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080832377-303208333.jpg)
`thenApply` 方法需要傳入核心引數為 `Function`型別。這個類核心方法為:
```java
R apply(T t)
```
所以這個介面將會把上一個任務返回結果當做入參,執行結束將會返回結果。
`thenAccept` 方法需要傳入引數物件為 `Consumer`型別,這個類核心方法為:
```java
void accept(T t)
```
返回值 `void` 可以看出,這個方法不支援返回結果,但是需要將上一個任務執行結果當做引數傳入。
`thenRun` 方法需要傳入引數物件為 `Runnable` 型別,這個類大家應該都比較熟悉,核心方法既不支援傳入引數,也不會返回執行結果。
`thenCompose` 方法作用與 `thenApply` 一樣,只不過 `thenCompose` 需要返回新的 `CompletionStage`。這麼理解比較抽象,可以集合程式碼一起理解。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080832532-1535986211.jpg)
方法中帶有 **Async** ,代表可以非同步執行,這個系列還有過載方法,可以傳入自定義的執行緒池,上圖未展示,讀者只可以自行檢視 API。
最後我們通過程式碼展示 `thenApply` 使用方式:
```java
CompletableFuture cf
= CompletableFuture.supplyAsync(() -> "hello,樓下小黑哥")// 1
.thenApply(s -> s + "@程式通事") // 2
.thenApply(String::toUpperCase); // 3
System.out.println(cf.join());
// 輸出結果 HELLO,樓下小黑哥@程式通事
```
這段程式碼比較簡單,首先我們開啟一個非同步任務,接著序列執行後續兩個任務。任務 2 需要等待任務1 執行完成,任務 3 需要等待任務 2。
> 上面方法,大家需要記住了 `Function`,`Consumer`,`Runnable` 三者區別,根據場景選擇使用。
### 1.6 AND 匯聚關係
AND 匯聚關係代表所有任務完成之後,才能進行下一個任務。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080832681-1554875019.jpg)
如上所示,只有任務 A 與任務 B 都完成之後,任務 C 才會開始執行。
`CompletionStage` 有以下介面描述這種關係。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080832840-2047568284.jpg)
`thenCombine` 方法核心引數 `BiFunction` ,作用與 `Function`一樣,只不過 `BiFunction` 可以接受兩個引數,而 `Function` 只能接受一個引數。
`thenAcceptBoth` 方法核心引數`BiConsumer` 作用也與 `Consumer`一樣,不過其需要接受兩個引數。
`runAfterBoth` 方法核心引數最簡單,上面已經介紹過,不再介紹。
這三組方法只能完成兩個任務 AND 匯聚關係,如果需要完成多個任務匯聚關係,需要使用 `CompletableFuture#allOf`,不過這裡需要注意,這個方法是不支援返回任務結果。
AND 匯聚關係相關示例程式碼,開頭已經使用過了,這裡再貼上一下,方便大家理解:
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080830551-292151003.jpg)
### 1.7 OR 匯聚關係
有 AND 匯聚關係,當然也存在 OR 匯聚關係。OR 匯聚關係代表只要多個任務中任一任務完成,就可以接著接著執行下一任務。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080833275-1582656936.jpg)
`CompletionStage` 有以下介面描述這種關係:
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080833438-320401663.jpg)
前面三組介面方法傳參與 AND 匯聚關係一致,這裡也不再詳細解釋了。
當然 OR 匯聚關係可以使用 `CompletableFuture#anyOf` 執行多個任務。
下面示例程式碼展示如何使用 `applyToEither` 完成 OR 關係。
```java
CompletableFuture cf
= CompletableFuture.supplyAsync(() -> {
sleep(5, TimeUnit.SECONDS);
return "hello,樓下小黑哥";
});// 1
CompletableFuture cf2 = cf.supplyAsync(() -> {
sleep(3, TimeUnit.SECONDS);
return "hello,程式通事";
});
// 執行 OR 關係
CompletableFuture cf3 = cf2.applyToEither(cf, s -> s);
// 輸出結果,由於 cf2 只休眠 3 秒,優先執行完畢
System.out.println(cf2.join());
// 結果:hello,程式通事
```
### 1.8 異常處理
`CompletableFuture` 方法執行過程若產生異常,當呼叫 `get`,`join `獲取任務結果才會丟擲異常。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080833701-1718647955.jpg)
上面程式碼我們顯示使用 `try..catch` 處理上面的異常。不過這種方式不太優雅,`CompletionStage` 提供幾個方法,可以優雅處理異常。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080833846-944666818.jpg)
`exceptionally` 使用方式類似於 `try..catch` 中 `catch`程式碼塊中異常處理。
`whenComplete` 與 `handle` 方法就類似於 `try..catch..finanlly` 中 `finally` 程式碼塊。無論是否發生異常,都將會執行的。這兩個方法區別在於 `handle` 支援返回結果。
下面示例程式碼展示 `handle` 用法:
```java
CompletableFuture
f0 = CompletableFuture.supplyAsync(() -> (7 / 0))
.thenApply(r -> r * 10)
.handle((integer, throwable) -> {
// 如果異常存在,列印異常,並且返回預設值
if (throwable != null) {
throwable.printStackTrace();
return 0;
} else {
// 如果
return integer;
}
});
System.out.println(f0.join());
/**
*java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
* .....
*
* 0
*/
```
## 0x02. 總結
JDK8 提供 `CompletableFuture` 功能非常強大,可以編排非同步任務,完成序列執行,並行執行,AND 匯聚關係,OR 匯聚關係。
不過這個類方法實在太多,且方法還需要傳入各種函式式介面,新手剛開始使用會直接會被弄懵逼。這裡幫大家在總結一下三類核心引數的作用
- `Function` 這類函式介面既支援接收引數,也支援返回值
- `Consumer` 這類介面函式只支援接受引數,不支援返回值
- `Runnable` 這類介面不支援接受引數,也不支援返回值
搞清楚函式引數作用以後,然後根據序列,AND 匯聚關係,OR 匯聚關係歸納一下相關方法,這樣就比較好理解了
最後再貼一下,文章開頭的思維導圖,希望對你有幫助。
![](https://img2020.cnblogs.com/other/1419561/202003/1419561-20200309080834023-515296793.jpg)
## 0x03. 幫助文件
1. 極客時間-併發程式設計專欄
2. https://colobu.com/2016/02/29/Java-CompletableFuture
3. https://www.ibm.com/developerworks/cn/java/j-cf-of-jdk8/index.html
## 最後說一句(求關注)
`CompletableFuture` 很早之前就有關注,本以為跟 `Future`一樣,使用挺簡單,誰知道學的時候才發現好難。各種 API 方法看的頭有點大。
後來看到極客時間-『併發程式設計』專欄使用歸納方式分類 `CompletableFuture` 各種方法,一下子就看懂了。所這篇文章也參考這種歸納方式。
這篇文章找資料,整理一個星期,幸好今天順利產出。
看在小黑哥寫的這麼辛苦的份上,點個關注吧,賞個讚唄。別下次一定啊,大哥!寫文章很辛苦的,需要來點正反饋。
才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
感謝您的閱讀,**我堅持原創**,十分歡迎並感謝您的關注~
> 歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:[studyidea.cn](https://studyi