Java CompletableFuture使用方式
在我之前的文章IO密集型服務(wù)提升性能的三種方法中提到過,提升IO密集型應(yīng)用性能有個(gè)方式就是異步編程,實(shí)現(xiàn)異步時(shí)一定會(huì)用到Future,使用多線程+Future我們可以讓多個(gè)任務(wù)同時(shí)去執(zhí)行,最后統(tǒng)一去獲取執(zhí)行結(jié)果,這樣整體執(zhí)行的時(shí)長就取決于最長的一個(gè)任務(wù),比如如下代碼:
public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(2); System.out.println(new Date()); Future<String> future1 = executorService.submit(() -> { Thread.sleep(5000); return new Date() + ":" + "thread1 result"; }); Future<String> future2 = executorService.submit(() -> { Thread.sleep(3000); return new Date() + ":" + "thread2 result"; }); try { System.out.println(future1.get()); System.out.println(future2.get()); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { throw new RuntimeException(e); } }
上面代碼的運(yùn)行時(shí)長是5s,如果兩個(gè)任務(wù)串行執(zhí)行的化,運(yùn)行時(shí)間就會(huì)是8s,當(dāng)多個(gè)任務(wù)多線程異步執(zhí)行時(shí),最終總的執(zhí)行時(shí)長取決于最長任務(wù)的執(zhí)行時(shí)長。Future在大部分異步變成情況下,已經(jīng)能很好的滿足我們異步變成的訴求了,但當(dāng)我看到CompletableFuture這個(gè)東西的時(shí)候,才發(fā)現(xiàn)Future還是太簡(jiǎn)單,我還是太年輕。
CompletableFuture除了和Future一樣可以獲取執(zhí)行結(jié)果外,它還**簡(jiǎn)化了異常、提供了手動(dòng)設(shè)置結(jié)果的接口、鏈?zhǔn)讲僮鳌⒔Y(jié)果組合、回調(diào),**接下來我們通過一些代碼示例,來看下CompletableFuture這些特性如何吊打Future。
異常處理
在使用Future時(shí),你只能在get()的時(shí)候抓取到異常,異常處理會(huì)和結(jié)果獲取的邏輯混在一起,有些時(shí)候處理起來會(huì)比較麻煩,而CompletableFuture提供了exceptionally
和handle
兩個(gè)api,可以很方便的添加異常處理邏輯,后續(xù)只需要直接使用get獲取結(jié)果即可,代碼示例如下:
public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // 故意拋出一個(gè)異常 if (System.currentTimeMillis() % 2 == 0) { throw new RuntimeException("Exception occurred!"); } return "Result"; }, executorService); future = future.exceptionally(ex -> { // 對(duì)異常進(jìn)行處理,并提供一個(gè)異常時(shí)的結(jié)果 return "Default Value"; }); try { System.out.println(future.get()); // 輸出 "Default Value" } catch (Exception e) { e.printStackTrace(); } }
exceptionally api讓任務(wù)在運(yùn)行過程中,如果抓到了Exception,就可以調(diào)用你給定的邏輯來處理異常。
與exceptionally就是handle方法,達(dá)到的效果一致,但使用略有不同,可以看出handle這個(gè)api是在任務(wù)執(zhí)行完成后,講exception和執(zhí)行結(jié)果共同處理一次。
future = future.handle((result, ex) -> { if (ex != null) { return "Default Value"; } return result; }); try { System.out.println(future.get()); // 輸出 "Default Value" } catch (Exception e) { e.printStackTrace(); }
手動(dòng)設(shè)置結(jié)果
在Future
中,我們無法手動(dòng)地完成一個(gè)任務(wù)或者設(shè)置任務(wù)的結(jié)果,這個(gè)任務(wù)的執(zhí)行結(jié)果完全取決于任務(wù)的執(zhí)行情況。
但在CompletableFuture
,我們可以在任何時(shí)間點(diǎn)上手動(dòng)設(shè)置結(jié)果或者標(biāo)記任務(wù)為已完成。
舉個(gè)例子,我要通過接口獲取某個(gè)數(shù)據(jù),但很要求時(shí)效性,如果超過1s我就不要接口調(diào)用結(jié)果了,而是返回一個(gè)默認(rèn)值。
Future設(shè)置等待時(shí)間也可以實(shí)現(xiàn)類似的功能,但不是很直觀, CompletableFuture
就很簡(jiǎn)單直觀了。
public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { // 隨機(jī)sleep 0-2s Thread.sleep(ThreadLocalRandom.current().nextInt(2000)); } catch (InterruptedException e) { throw new RuntimeException(e); } return "Result"; }, executorService); CompletableFuture<String> finalFuture = future; // 1s后手動(dòng)完成 executorService.submit(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 手動(dòng)地完成這個(gè) CompletableFuture,并設(shè)置計(jì)算結(jié)果 finalFuture.complete("Default Value"); }); try { System.out.println(future.get()); // 概率性輸出 "Default Value" } catch (Exception e) { e.printStackTrace(); } }
鏈?zhǔn)讲僮?/h2>
鏈?zhǔn)讲僮饕馕吨憧梢詫⒁粋€(gè)CompletableFuture
的結(jié)果直接傳遞給另一個(gè)異步操作,而無需等待第一個(gè)操作完成。
這使得你可以更容易地編寫復(fù)雜的并發(fā)邏輯,這種非常適合需要將多個(gè)接口結(jié)果串起來的流程,有點(diǎn)像langChain,我們直接看示例代碼:
public static void main(String[] args) { CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // 第一個(gè)異步任務(wù) return "Hello"; }).thenApplyAsync(result -> { // 第二個(gè)異步任務(wù),依賴第一個(gè)任務(wù)的結(jié)果 return result + " World"; }).thenApplyAsync(result -> { // 第三個(gè)異步任務(wù),依賴第二個(gè)任務(wù)的結(jié)果 return result + "!"; }); try { System.out.println(future.get()); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { throw new RuntimeException(e); } }
這個(gè)示例比較簡(jiǎn)單,在實(shí)際使用中,我們可以將三個(gè)任務(wù)替換成任何需要相互依賴的任務(wù),你只需要定義每個(gè)任務(wù)的輸入和輸出,然后將它們連接在一起,就可以創(chuàng)建一個(gè)復(fù)雜的并發(fā)流程。
結(jié)果組合
CompletableFuture
提供了幾個(gè)方法來組合或合并多個(gè)CompletableFuture
的結(jié)果。
最常用的可能是thenCombine
和allOf
方法。
以下是thenCombine
方法的使用示例:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { return "Hello"; }); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> { return "World!"; }); CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (s1, s2) -> { return s1 + ", " + s2; }); try { String result = combinedFuture.get(); System.out.println(result); // 輸出 "Hello, World!" } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }
在上述代碼中,我們首先創(chuàng)建了兩個(gè)獨(dú)立的CompletableFuture
,然后使用thenCombine
方法將它們的結(jié)果組合在一起。
以下是allOf
方法的使用示例:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { return "Hello"; }); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> { return "World!"; }); CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2); // 當(dāng)所有的 CompletableFuture 都完成時(shí),獲取結(jié)果 allFutures.thenRun(() -> { try { String result1 = future1.get(); String result2 = future2.get(); System.out.println(result1 + ", " + result2); // 輸出 "Hello, World!" } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } });
可以看出thenCombine和allOf都實(shí)現(xiàn)了類似的功能,但兩者的側(cè)重點(diǎn)不同,thenCombine實(shí)現(xiàn)的是對(duì)執(zhí)行結(jié)果的聚合,而allOf實(shí)現(xiàn)的是對(duì)執(zhí)行完?duì)顟B(tài)的聚合,還是需要你去顯式調(diào)用get()方法去獲取結(jié)果的,所以你還是得根據(jù)具體需求來選擇使用thenCombine
還是allOf
。
回調(diào)
使用Future時(shí),我們只能通過get()或者isDone()方法來獲取到任務(wù)是否執(zhí)行完成了,無法讓任務(wù)執(zhí)行完成后主動(dòng)通知到我們,但CompletableFuture提供了一些回調(diào)方法,可以讓任務(wù)執(zhí)行完成后執(zhí)行結(jié)果處理的任務(wù)或者執(zhí)行一些通知的邏輯。
CompletableFuture
提供了幾個(gè)方法來設(shè)置回調(diào),如thenApply
, thenAccept
, 和 thenRun
。
具體看代碼示例:
public static void main(String[] args) { CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // 異步任務(wù) return "Hello, World!"; }); future.thenApply(result -> { // 當(dāng) future 完成時(shí),這個(gè)回調(diào)會(huì)被調(diào)用。 // result 參數(shù)是 future 的結(jié)果。 // 這個(gè)回調(diào)的結(jié)果會(huì)被包裝成一個(gè)新的 CompletableFuture。 return result.length(); }).thenAccept(length -> { // 當(dāng)前面的 CompletableFuture 完成時(shí),這個(gè)回調(diào)會(huì)被調(diào)用。 // length 參數(shù)是前面 CompletableFuture 的結(jié)果。 // 這個(gè)回調(diào)沒有返回值。 System.out.println(length); // 輸出 "Hello, World!" 的長度 }).thenRun(() -> { // 當(dāng)前面的 CompletableFuture 完成時(shí),這個(gè)回調(diào)會(huì)被調(diào)用。 // 這個(gè)回調(diào)沒有輸入?yún)?shù),也沒有返回值。 System.out.println("Done"); }); }
thenApply、thenAccept和thenRun都是在任務(wù)執(zhí)行完成后被調(diào)起,但他們定位有所不同,thenApply處理的結(jié)果可以被包裝成一個(gè)新的CompletableFuture,只后可以繼續(xù)鏈?zhǔn)秸{(diào)用。
但thenAccept只能接受輸入,無法提供輸出,所以他的定位是任務(wù)的收尾,比如可以將結(jié)果輸出。
而thenRun既沒有輸入也沒有輸出,所以它只是獲取到了任務(wù)執(zhí)行完的狀態(tài),任務(wù)的執(zhí)行結(jié)果是獲取不到的,所以它是最弱的。
總結(jié)
總結(jié)下本文,Future
盡管有用,但在功能上還是相對(duì)簡(jiǎn)單。
CompletableFuture
不僅提供了獲取執(zhí)行結(jié)果的能力,它還增添了異常處理、手動(dòng)完成、鏈?zhǔn)讲僮?、結(jié)果組合和回調(diào)等強(qiáng)大功能。
- 異常處理: 利用
exceptionally
和handle
方法,可以簡(jiǎn)化異常處理流程,使其更加直觀和易于管理。 - 手動(dòng)設(shè)置結(jié)果:
CompletableFuture
允許我們?cè)谌我鈺r(shí)間點(diǎn)手動(dòng)設(shè)置結(jié)果,增加了靈活性。 - 鏈?zhǔn)讲僮?/strong>: 通過鏈?zhǔn)讲僮?,我們可以將不同的異步操作以流水線的方式串聯(lián)起來,編寫復(fù)雜的并發(fā)邏輯變得更加簡(jiǎn)單。
- 結(jié)果組合:
thenCombine
和allOf
方法能夠?qū)⒍鄠€(gè)CompletableFuture
的結(jié)果合并,為我們提供了更多處理異步操作結(jié)果的方式。 - 回調(diào): 提供了
thenApply
,thenAccept
, 和thenRun
等方法,允許我們?cè)谌蝿?wù)完成后進(jìn)行結(jié)果處理或其他邏輯操作。
通過這些特性,CompletableFuture
顯著地提升了異步編程的能力和靈活性。無論是簡(jiǎn)化異常處理,還是實(shí)現(xiàn)復(fù)雜的異步邏輯,CompletableFuture
都能夠幫助我們寫出更清晰、更高效的代碼。我相信通過本文的示例代碼,你可以更深入地理解CompletableFuture
的強(qiáng)大之處,并在你的項(xiàng)目中充分利用它來提升性能。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot使用Shiro實(shí)現(xiàn)動(dòng)態(tài)加載權(quán)限詳解流程
本文小編將基于?SpringBoot?集成?Shiro?實(shí)現(xiàn)動(dòng)態(tài)uri權(quán)限,由前端vue在頁面配置uri,Java后端動(dòng)態(tài)刷新權(quán)限,不用重啟項(xiàng)目,以及在頁面分配給用戶?角色?、?按鈕?、uri?權(quán)限后,后端動(dòng)態(tài)分配權(quán)限,用戶無需在頁面重新登錄才能獲取最新權(quán)限,一切權(quán)限動(dòng)態(tài)加載,靈活配置2022-07-07Java前端Layer.open.btn驗(yàn)證無效解決方法
在本篇文章里我們給大家整理了一篇關(guān)于Java前端Layer.open.btn驗(yàn)證無效解決方法以及實(shí)例代碼,需要的朋友們可以參考學(xué)習(xí)下。2019-09-09java監(jiān)聽器的實(shí)現(xiàn)和原理詳解
這篇文章主要給大家介紹了關(guān)于java監(jiān)聽器實(shí)現(xiàn)和原理的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08java 全角半角字符轉(zhuǎn)換的方法實(shí)例
這篇文章主要介紹了java 全角半角字符轉(zhuǎn)換的方法,大家參考使用吧2013-11-11詳解SpringBoot+Dubbo集成ELK實(shí)戰(zhàn)
這篇文章主要介紹了詳解SpringBoot+Dubbo集成ELK實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10mybatis mapper互相引用resultMap啟動(dòng)出錯(cuò)的解決
這篇文章主要介紹了mybatis mapper互相引用resultMap啟動(dòng)出錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08java阻塞隊(duì)列BlockingQueue詳細(xì)解讀
這篇文章主要介紹了java阻塞隊(duì)列BlockingQueue詳細(xì)解讀,在新增的Concurrent包中,BlockingQueue很好的解決了多線程中,如何高效安全“傳輸”數(shù)據(jù)的問題,通過這些高效并且線程安全的隊(duì)列類,為我們快速搭建高質(zhì)量的多線程程序帶來極大的便利,需要的朋友可以參考下2023-10-10