簡介
CompletableFuture結合了Future的優點,提供了非常強大的Future的擴充套件功能,可以幫助我們簡化非同步程式設計的複雜性,提供了函數語言程式設計的能力,可以透過回撥的方式處理計算結果,並且提供了轉換和組合CompletableFuture的方法。
CompletableFuture被設計在Java中進行非同步程式設計。非同步程式設計意味著在主執行緒之外建立一個獨立的執行緒,與主執行緒分隔開,並在上面執行一個非阻塞的任務,然後通知主執行緒進展,成功或者失敗。
CompletableFuture是由Java8引入的,在Java8之前我們一般透過Future實現非同步。
Future用於表示非同步計算的結果,只能透過阻塞或者輪詢的方式獲取結果,而且不支援設定回撥方法,Java8之前若要設定回撥一般會使用guava的ListenableFuture。 CompletableFuture對Future進行了擴充套件,可以透過設定回撥的方式處理計算結果,同時也支援組合操作,支援進一步的編排,同時一定程度解決了回撥地獄的問題。
核心概念
CompletableFuture
是一個非常強大的併發工具類,它實現了 Future
和 CompletionStage
介面,用於表示某個非同步計算的結果,與傳統的 Future
不同,CompletableFuture
提供了函數語言程式設計的方法,可以更容易地組織非同步程式碼,處理回撥和組合多個非同步操作。
假設,有一個電商網站,使用者瀏覽產品詳情頁時,需要展示產品的基本資訊、價格、庫存、使用者評價等多個方面的資料,這些資料可能來自不同的資料來源或服務,比如:
- 產品基本資訊可能來自一個主資料庫。
- 價格和庫存 可能需要實時從另一個庫存服務獲取。
- 使用者評價可能儲存在另一個專門用於使用者反饋的系統中。
為了提升使用者體驗,希望這些資料的獲取能夠並行進行,而不是一個接一個地序列獲取,這就是 CompletableFuture
的經典場景。
CompletableFuture
類在主要用來解決非同步程式設計和併發執行的問題,在傳統的同步程式設計模型中,程式碼的執行通常是阻塞的,即一行程式碼執行完成後,下一行程式碼才能開始執行,這種模型在處理耗時操作時,如 I/O 操作、資料庫訪問或網路請求,會導致執行緒長時間閒置,等待操作完成,從而降低系統的吞吐量和響應能力。
因此,CompletableFuture
類提供了一種非阻塞的、基於回撥的程式設計方式,可以在等待某個長時間執行的任務完成時,同時執行其他任務,這樣,就可以更充分地利用系統資源,提高程式的併發性和響應速度。
使用CompletableFuture
通常用於解決以下類似場景的問題:
- 發起非同步請求:當使用者請求一個產品詳情頁時,後端服務可以同時發起對三個資料來源的非同步請求,這可以透過建立三個
CompletableFuture
例項來實現,每個例項負責一個資料來源的請求。 - 處理非同步結果:一旦這些非同步請求發出,它們就可以獨立地執行,主執行緒可以繼續處理其他任務,當某個
CompletableFuture
完成時,它會包含一個結果(或者是執行過程中的異常)。 - 組合非同步結果:使用
CompletableFuture
的組合方法(如thenCombine
、thenAcceptBoth
或allOf
),可以等待所有非同步操作完成,並將它們的結果組合在一起,比如,可以等待產品基本資訊、價格和庫存以及使用者評價都返回後,再將這些資料整合到一個響應物件中,返回給前端。 - 異常處理:如果在獲取某個資料來源時發生異常,
CompletableFuture
允許以非同步的方式處理這些異常,比如透過exceptionally
方法提供一個預設的備選結果或執行一些清理操作。 - 最終響應:一旦所有資料來源的資料都成功獲取並組合在一起,或者某個資料來源發生異常並得到了妥善處理,服務就可以將最終的產品詳情頁響應傳送給前端使用者。
使用CompletableFuture
可以高效的併發資料獲取,提升系統的響應速度和整體效能。
核心API
CompletableFuture
列用於表示某個非同步計算的結果,它提供了函數語言程式設計的方法來處理非同步計算,允許以非阻塞的方式編寫併發程式碼,並且可以連結多個非同步操作,以下是一些常用方法的含義:
1、靜態工廠方法
CompletableFuture.supplyAsync(Supplier<? extends U> supplier)
: 非同步執行給定的Supplier
,並返回一個表示結果的新CompletableFuture
。CompletableFuture.supplyAsync(Supplier<? extends U> supplier, Executor executor)
: 使用指定的執行器非同步執行給定的Supplier
。CompletableFuture.runAsync(Runnable runnable)
: 非同步執行給定的Runnable
,並返回一個表示其完成的新CompletableFuture
。CompletableFuture.runAsync(Runnable runnable, Executor executor)
: 使用指定的執行器非同步執行給定的Runnable
。
2、完成時的處理
thenApply(Function<? super T,? extends U> fn)
: 當此CompletableFuture
完成時,對其結果應用給定的函式。thenAccept(Consumer<? super T> action)
: 當此CompletableFuture
完成時,執行給定的操作。thenRun(Runnable action)
: 當此CompletableFuture
完成時,執行給定的無引數操作。
3、異常處理
exceptionally(Function<Throwable,? extends T> fn)
: 當此CompletableFuture
異常完成時,對其異常應用給定的函式。
4、組合多個 CompletableFuture
thenCombine(CompletableFuture<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
: 當此CompletableFuture
和另一個都完成時,使用給定的函式組合它們的結果。thenAcceptBoth(CompletableFuture<? extends U> other, BiConsumer<? super T,? super U> action)
: 當此CompletableFuture
和另一個都完成時,對它們的結果執行給定的操作。runAfterBoth(CompletableFuture<?> other, Runnable action)
: 當此CompletableFuture
和另一個都完成時,執行給定的操作。applyToEither(CompletableFuture<? extends T> other, Function<? super T, U> fn)
: 當此CompletableFuture
或另一個完成時(哪個先完成),對其結果應用給定的函式。acceptEither(CompletableFuture<? extends T> other, Consumer<? super T> action)
: 當此CompletableFuture
或另一個完成時(哪個先完成),對其結果執行給定的操作。runAfterEither(CompletableFuture<?> other, Runnable action)
: 當此CompletableFuture
或另一個完成時(哪個先完成),執行給定的操作。
5、等待和獲取結果
get()
: 等待計算完成,然後獲取其結果。get(long timeout, TimeUnit unit)
: 等待計算在給定的時間內完成,並獲取其結果。join()
: 類似於get()
,但是會在計算未完成時丟擲未檢查的異常。complete(T value)
: 如果尚未完成,則設定此CompletableFuture
的結果。completeExceptionally(Throwable ex)
: 如果尚未完成,則使此CompletableFuture
異常完成。
6、取消
cancel(boolean mayInterruptIfRunning)
: 嘗試取消此CompletableFuture
。isCancelled()
: 如果此CompletableFuture
被取消,則返回true
。
7、查詢
isDone()
: 如果此CompletableFuture
完成(無論是正常完成還是異常完成),則返回true
。
封裝計算邏輯的CompletableFuture
上面的程式碼允許我們選擇任何併發執行的機制,但是如果我們想跳過這個樣板檔案,簡單地非同步執行一些程式碼呢?
靜態方法runAsync和supplyAsync允許我們相應地使用Runnable和Supplier函式型別建立一個可完成的未來例項。
Runnable和Supplier都是函式介面,由於新的java8特性,它們允許將例項作為lambda表示式傳遞。
Runnable介面與執行緒中使用的舊介面相同,不允許返回值。
Supplier介面是一個通用函式介面,它有一個方法,該方法沒有引數,並且返回一個引數化型別的值。
這允許我們提供一個供應商例項作為lambda表示式來執行計算並返回結果。簡單到:
CompletableFuture<String> future
= CompletableFuture.supplyAsync(() -> "Hello");
// ...
assertEquals("Hello", future.get());
非同步計算的處理結果
處理計算結果的最通用的方法是將其提供給函式。thenApply方法正是這樣做的;它接受一個函式例項,用它來處理結果,並返回一個包含函式返回值的Future:
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future = completableFuture
.thenApply(s -> s + " World");
assertEquals("Hello World", future.get());
如果我們不需要在Future中返回值,我們可以使用Consumer函式介面的例項。它的單個方法接受一個引數並返回void。
在可完成的將來,有一種方法可以解決這個用例。thenAccept方法接收使用者並將計算結果傳遞給它。最後一個future.get()呼叫返回Void型別的例項:
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<Void> future = completableFuture
.thenAccept(s -> System.out.println("Computation returned: " + s));
future.get();
最後,如果我們既不需要計算的值,也不想返回值,那麼我們可以將一個可執行的lambda傳遞給thenRun方法。在下面的示例中,我們只需在呼叫future.get()後在控制檯中列印一行:
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<Void> future = completableFuture
.thenRun(() -> System.out.println("Computation finished."));
future.get();
組合CompletableFuture
CompletableFuture API最好的部分是能夠在一系列計算步驟中組合CompletableFuture例項。
這種連結的結果本身就是一個完整的Future,允許進一步的連結和組合。這種方法在函式語言中普遍存在,通常被稱為享元模式。
在下面的示例中,我們使用thenCompose方法按順序連結兩個Future。
請注意,此方法接受一個返回CompletableFuture例項的函式。此函式的引數是上一計算步驟的結果。這允許我們在下一個CompletableFuture的lambda中使用此值:
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello")
.thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World"));
assertEquals("Hello World", completableFuture.get());
thenCompose方法與thenApply一起實現了享元模式的基本構建塊。它們與流的map和flatMap方法以及java8中的可選類密切相關。
兩個方法都接收一個函式並將其應用於計算結果,但是thencomose(flatMap)方法接收一個返回另一個相同型別物件的函式。這種功能結構允許將這些類的例項組合為構建塊。
如果我們想執行兩個獨立的未來,並對它們的結果進行處理,我們可以使用thenCombine方法,該方法接受一個未來和一個具有兩個引數的函式來處理這兩個結果:
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(() -> "Hello")
.thenCombine(CompletableFuture.supplyAsync(
() -> " World"), (s1, s2) -> s1 + s2));
assertEquals("Hello World", completableFuture.get());
一個簡單的例子是,當我們想處理兩個CompletableFuture的結果時,但不需要將任何結果值傳遞給CompletableFuture的鏈。thenAcceptBoth方法可以幫助:
CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
.thenAcceptBoth(CompletableFuture.supplyAsync(() -> " World"),
(s1, s2) -> System.out.println(s1 + s2));
thenApply()和thenCompose()方法之間的區別
在前面的部分中,我們展示了有關thenApply()和thenCompose()的示例。兩個api都有助於連結不同的CompletableFuture呼叫,但這兩個函式的用法不同。
thenApply()
我們可以使用此方法處理上一次呼叫的結果。但是,需要記住的一點是,返回型別將由所有呼叫組合而成。
因此,當我們要轉換CompletableFuture呼叫的結果時,此方法非常有用:
CompletableFuture<Integer> finalResult = compute().thenApply(s-> s + 1);
thenCompose()
thenCompose()方法與thenApply()類似,因為兩者都返回一個新的完成階段。但是,thencose()使用前一階段作為引數。它將展平並直接返回一個帶有結果的CompletableFuture,而不是我們在thenApply()中觀察到的巢狀CompletableFuture:
CompletableFuture<Integer> computeAnother(Integer i){
return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture<Integer> finalResult = compute().thenCompose(this::computeAnother);
因此,如果要連結可完成的CompletableFuture方法,那麼最好使用thenCompose()。
另外,請注意,這兩個方法之間的差異類似於map()和flatMap()之間的差異。
並行執行多個CompletableFuture
當我們需要並行執行多個期貨時,我們通常希望等待所有Supplier執行,然後處理它們的組合結果。
CompletableFuture.allOf靜態方法允許等待的所有Supplier的完成:
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。這種方法的侷限性在於它不能返回所有Supplier的組合結果。相反,我們必須從未來手動獲取結果。幸運的是,CompletableFuture.join()方法和Java 8 Streams API使它變得簡單:
String combined = Stream.of(future1, future2, future3)
.map(CompletableFuture::join)
.collect(Collectors.joining(" "));
assertEquals("Hello Beautiful World", combined);
join()方法類似於get方法,但是如果Future不能正常完成,它會丟擲一個未檢查的異常。這樣就可以將其用作Stream.map()方法中的方法引用。
具體使用簡單demo案例
/** * 獲取統計指標資訊 * @return 統計指標值 */ @GetMapping("/statistics") public ResultVO statistics(){ ResultVO resultVO = new ResultVO(); try { // 獲取企業所需要統計的數量 CompletableFuture<Integer> unitNum = CompletableFuture.supplyAsync(() -> { log.info("執行[獲取企業所需要統計的數量]任務:"+"執行緒id:"+Thread.currentThread().getId()+"執行緒名稱:"+ Thread.currentThread().getName()); return enterpriseService.list().size(); }); // 獲取供需所需要統計的數量 CompletableFuture<Integer> supplyDemandNum = CompletableFuture.supplyAsync(() -> { log.info("執行[獲取供需所需要統計的數量]任務:"+"執行緒id:"+Thread.currentThread().getId()+"執行緒名稱:"+ Thread.currentThread().getName()); return supplyDemandService.list().size(); }); // 等待所有非同步任務執行完成 CompletableFuture.allOf(unitNum,supplyDemandNum).join(); resultVO.setUnitNum(unitNum.get()); resultVO.setSupplyDemandNum(supplyDemandNum.get()); } catch (Exception e) { log.error("任務執行異常!"); throw new RuntimeException("獲取統計指標異常!"); } return resultVO; }
返回結果:
{ "unitNum": 109, "supplyDemandNum": 2 }