《Java8实战》笔记(11):CompletableFuture-组合式异步编程
CompletableFuture:組合式異步編程
最近這些年,兩種趨勢不斷地推動我們反思我們設計軟件的方式。
我們注意到隨著多核處理器的出現,提升應用程序處理速度最有效的方式是編寫能充分發揮多核能力的軟件。
你已經看到通過切分大型的任務,讓每個子任務并行運行,這一目標是能夠實現的;你也已經了解相對直接使用線程的方式,使用分支/合并框架(在Java 7中引入)和并行流(在Java 8中新引入)能以更簡單、更有效的方式實現這一目標。
第二種趨勢反映在公共API日益增長的互聯網服務應用。
著名的互聯網大鱷們紛紛提供了自己的公共API服務,比如
現在,很少有網站或者網絡應用會以完全隔離的方式工作。更多的時候,我們看到的下一代網絡應用都采用“混聚”(mash-up)的方式:它會使用來自多個來源的內容,將這些內容聚合在一起,方便用戶的生活。
比如,你可能希望為你的法國客戶提供指定主題的熱點報道。為實現這一功能,你需要向谷歌或者Twitter的API請求所有語言中針對該主題最熱門的評論,可能還需要依據你的內部算法對它們的相關性進行排序。之后,你可能還需要使用谷歌的翻譯服務把它們翻譯成法語,甚至利用谷歌地圖服務定位出評論作者的位置信息,最終將所有這些信息聚集起來,呈現在你的網站上。
當然,如果某些外部網絡服務發生響應慢的情況,你希望依舊能為用戶提供部分信息,比如提供帶問號標記的通用地圖,以文本的方式顯示信息,而不是呆呆地顯示一片空白屏幕,直到地圖服務器返回結果或者超時退出。
要實現類似的服務,你需要與互聯網上的多個Web服務通信。可是,你并不希望因為等待某些服務的響應,阻塞應用程序的運行,浪費數十億寶貴的CPU時鐘周期。比如,不要因為等待Facebook的數據,暫停對來自Twitter的數據處理。
這些場景體現了多任務程序設計的另一面。分支/合并框架以及并行流是實現并行處理的寶貴工具;它們將一個操作切分為多個子操作,在多個不同的核、CPU甚至是機器上并行地執行這些子操作。
與此相反,如果你的意圖是實現并發,而非并行,或者你的主要目標是在同一個CPU上執行幾個松耦合的任務,充分利用CPU的核,讓其足夠忙碌,從而最大化程序的吞吐量,那么你其實真正想做的是避免因為等待遠程服務的返回,或者對數據庫的查詢,而阻塞線程的執行,浪費寶貴的計算資源,因為這種等待的時間很可能相當長。Future接口,尤其是它的新版實現CompletableFuture,是處理這種情況的利器。
Future接口
Future接口在Java 5中被引入,設計初衷是對將來某個時刻會發生的結果進行建模。它建模了一種異步計算,返回一個執行運算結果的引用,當運算結束后,這個引用被返回給調用方。在Future中觸發那些潛在耗時的操作把調用線程解放出來,讓它能繼續執行其他有價值的工作,不再需要呆呆等待耗時的操作完成。
打個比方,你可以把它想象成這樣的場景:你拿了一袋子衣服到你中意的干洗店去洗。干洗店的員工會給你張發票,告訴你什么時候你的衣服會洗好(這就是一個Future事件)。衣服干洗的同時,你可以去做其他的事情。
Future的另一個優點是它比更底層的Thread更易用。要使用Future,通常你只需要將耗時的操作封裝在一個Callable對象中,再將它提交給ExecutorService,就萬事大吉了。
ExecutorService executor = Executors.newCachedThreadPool(); Future<Double> future = executor.submit(new Callable<Double>() {public Double call() {//以異步方式在新的線程中執行耗時的操作return doSomeLongComputation();} });//異步操作進行的同時,你可以做其他的事情 doSomethingElse();try {//獲取異步操作的結果,如果最終被阻塞,無法得到結果,那么在最多等待1秒鐘之后退出Double result = future.get(1, TimeUnit.SECONDS); } catch (ExecutionException ee) {// 計算拋出一個異常 } catch (InterruptedException ie) {// 當前線程在等待過程中被中斷 } catch (TimeoutException te) {// 在Future對象完成之前超過已過期 }如果該長時間運行的操作永遠不返回了會怎樣?
為了處理這種可能性,雖然Future提供了一個無需任何參數的get方法,還是推薦大家使用重載版本的get方法,它接受一個超時的參數,通過它,你可以定義你的線程等待Future結果的最長時間
Future接口的局限性
比如,很難表述Future結果之間的依賴性;
從文字描述上這很簡單,“當長時間計算任務完成時,請將該計算的結果通知到另一個長時間運行的計算任務,這兩個計算任務都完成后,將計算的結果與另一個查詢操作結果合并”。
但是,使用Future中提供的方法完成這樣的操作又是另外一回事。這也是我們需要更具描述能力的特性的原因,比如:
-
將兩個異步計算合并為一個——這兩個異步計算之間相互獨立,同時第二個又依賴于第一個的結果。
-
等待Future集合中的所有任務都完成。
-
僅等待Future集合中最快結束的任務完成(有可能因為它們試圖通過不同的方式計算同一個值),并返回它的結果。
-
通過編程方式完成一個Future任務的執行(即以手工設定異步操作結果的方式)。
-
應對Future的完成事件(即當Future的完成事件發生時會收到通知,并能使用Future計算的結果進行下一步的操作,不只是簡單地阻塞等待操作的結果)。
新的CompletableFuture類(它實現了Future接口)如何利用Java 8的新特性以更直觀的方式將上述需求都變為可能。Stream和CompletableFuture的設計都遵循了類似的模式:它們都使用了Lambda表達式以及流水線的思想。
使用CompletableFuture構建異步應用
為了展示CompletableFuture的強大特性,我們會創建一個名為“最佳價格查詢器”(best-price-finder)的應用,它會查詢多個在線商店,依據給定的產品或服務找出最低的價格。這個過程中,你會學到幾個重要的技能。
-
首先,你會學到如何為你的客戶提供異步API。
-
其次,你會掌握如何讓你使用了同步API的代碼變為非阻塞代碼。你會了解如何使用流水線將兩個接續的異步操作合并為一個異步計算操作。比如,在線商店返回了你想要購買商品的原始價格,并附帶著一個折扣代碼——最終,要計算出該商品的實際價格,你不得不訪問第二個遠程折扣服務,查詢該折扣代碼對應的折扣比率。
-
你還會學到如何以響應式的方式處理異步操作的完成事件,以及隨著各個商店返回它的商品價格,最佳價格查詢器如何持續地更新每種商品的最佳推薦,而不是等待所有的商店都返回他們各自的價格(這種方式存在著一定的風險,一旦某家商店的服務中斷,用戶可能遭遇白屏)。
同步API與異步API
同步API其實只是對傳統方法調用的另一種稱呼:你調用了某個方法,調用方在被調用方運行的過程中會等待,被調用方運行結束返回,調用方取得被調用方的返回值并繼續運行。即使調用方和被調用方在不同的線程中運行,調用方還是需要等待被調用方結束運行,這就是阻塞式調用這個名詞的由來。
與此相反,異步API會直接返回,或者至少在被調用方計算完成之前,將它剩余的計算任務交給另一個線程去做,該線程和調用方是異步的——這就是非阻塞式調用的由來。執行剩余計算任務的線程會將它的計算結果返回給調用方。返回的方式要么是通過回調函數,要么是由調用方再次執行一個“等待,直到計算完成”的方法調用。這種方式的計算在I/O系統程序設計中非常常見:你發起了一次磁盤訪問,這次訪問和你的其他計算操作是異步的,你完成其他的任務時,磁盤塊的數據可能還沒載入到內存,你只需要等待數據的載入完成。
實現異步API
Shop
首先,商店應該聲明依據指定產品名稱返回價格的方法:
public class Shop {public double getPrice(String product) {// 待實現} }模擬1秒鐘延遲的方法
public static void delay() {try {Thread.sleep(1000L);} catch (InterruptedException e) {throw new RuntimeException(e);} }在getPrice方法中引入一個模擬的延遲
public double getPrice(String product) {return calculatePrice(product); }private double calculatePrice(String product) {delay();return random.nextDouble() * product.charAt(0) + product.charAt(1); }將同步方法轉換為異步方法
將getPrice轉換為getPriceAsync方法,并修改它的返回值:
public Future<Double> getPriceAsync(String product) {CompletableFuture<Double> futurePrice = new CompletableFuture<>();new Thread( () -> {double price = calculatePrice(product);futurePrice.complete(price);}).start();//無需等待還沒結束的計算,直接返回Future對象return futurePrice; }使用異步API
ShopMain
運行結果
Invocation returned after 43 msecs Price is 123.26 Price returned after 1045 msecs錯誤處理
如果價格計算過程中產生了錯誤會怎樣呢?非常不幸,這種情況下你會得到一個相當糟糕的結果:用于提示錯誤的異常會被限制在試圖計算商品價格的當前線程的范圍內,最終會殺死該線程,而這會導致等待get方法返回結果的客戶端永久地被阻塞。
客戶端可以使用重載版本的get方法,它使用一個超時參數來避免發生這樣的情況。使用這種方法至少能防止程序永久地等待下去,超時發生時,程序會得到通知發生了TimeoutException。
為了讓客戶端能了解商店無法提供請求商品價格的原因,你需要使用CompletableFuture的completeExceptionally方法將導致CompletableFuture內發生問題的異常拋出。
public Future<Double> getPriceAsync(String product) {CompletableFuture<Double> futurePrice = new CompletableFuture<>();new Thread( () -> {try {double price = calculatePrice(product);futurePrice.complete(price);} catch (Exception ex) {futurePrice.completeExceptionally(ex);}}).start();return futurePrice; }使用工廠方法supplyAsync創建CompletableFuture
CompletableFuture類自身提供了大量精巧的工廠方法,使用這些方法能更容易地完成整個流程,還不用擔心實現的細節。
public Future<Double> getPriceAsync(String product) {return CompletableFuture.supplyAsync(() -> calculatePrice(product)); }讓你的代碼免受阻塞之苦——將無法改變的同步適配成異步
BestPriceFinder
BestPriceFinderMain
你已經被要求進行“最佳價格查詢器”應用的開發了,不過你需要查詢的所有商店只提供了同步API。
換句話說,你有一個商家的列表,如下所示:
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),new Shop("LetsSaveBig"),new Shop("MyFavoriteShop"),new Shop("BuyItAll"));采用順序查詢所有商店的方式實現的findPrices方法
public List<String> findPrices(String product) {return shops.stream().map(shop -> String.format("%s price is %.2f",shop.getName(), shop.getPrice(product))).collect(toList()); }驗證findPrices的正確性和執行性能
long start = System.nanoTime(); System.out.println(findPrices("myPhone27S")); long duration = (System.nanoTime() - start) / 1_000_000; System.out.println("Done in " + duration + " msecs");運行結果:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price is 214.13, BuyItAll price is 184.74] Done in 4032 msecs使用并行流對請求進行并行操作
public List<String> findPrices(String product) {return shops.parallelStream().map(shop -> String.format("%s price is %.2f",shop.getName(), shop.getPrice(product))).collect(toList()); }使用CompletableFuture發起異步請求
public List<String> findPrices(String product) {List<CompletableFuture<String>> priceFutures = shops.stream().map(shop -> CompletableFuture.supplyAsync(() -> shop.getName() + " price is " + shop.getPrice(product))).collect(Collectors.toList());return priceFutures.stream().map(CompletableFuture::join).collect(toList()); }尋找更好的方案
execute("sequential", () -> bestPriceFinder.findPricesSequential("myPhone27S")); execute("parallel", () -> bestPriceFinder.findPricesParallel("myPhone27S")); execute("composed CompletableFuture", () -> bestPriceFinder.findPricesFuture("myPhone27S")); execute("composed CompletableFuture2", () -> bestPriceFinder.findPricesFuture2("myPhone27S"));運行結果
[BestPrice price is 123.25651664705744, LetsSaveBig price is 169.4653393606115, MyFavoriteShop price is 214.12914480588853, BuyItAll price is 184.74384995303313] sequential done in 4076 msecs[BestPrice price is 197.15388829450728, LetsSaveBig price is 167.59404755738808, MyFavoriteShop price is 192.48730292081552, BuyItAll price is 199.67823140124116] parallel done in 2013 msecs[BestPrice price is 171.10524235618578, LetsSaveBig price is 168.59369176671822, MyFavoriteShop price is 174.79155890558252, BuyItAll price is 154.82955565763797] composed CompletableFuture done in 1011 msecs[BestPrice price is 227.53480147033423, LetsSaveBig price is 200.89398407500244, MyFavoriteShop price is 161.14747297059597, BuyItAll price is 155.9041805933185] composed CompletableFuture2 done in 1005 msecs它們看起來不相伯仲,究其原因都一樣:它們內部采用的是同樣的通用線程池,默認都使用固定數目的線程,具體線程數取決于Runtime.getRuntime().availableProcessors()的返回值。然而,CompletableFuture具有一定的優勢,因為它允許你對執行器(Executor)進行配置,尤其是線程池的大小,讓它以更適合應用需求的方式進行配置,滿足程序的要求,而這是并行流API無法提供的。
使用定制的執行器
創建一個配有線程池的執行器,線程池中線程的數目取決于你預計你的應用需要處理的負荷,但是你該如何選擇合適的線程數目呢?
調整線程池的大小
《Java并發編程實戰》一書中,Brian Goetz和合著者們為線程池大小的優化提供了不少中肯的建議。這非常重要,如果線程池中線程的數量過多,最終它們會競爭稀缺的處理器和內存資源,浪費大量的時間在上下文切換上。反之,如果線程的數目過少,正如你的應用所面臨的情況,處理器的一些核可能就無法充分利用。Brian Goetz建議,線程池大小與處理器的利用率之比可以使用下面的公式進行估算:
N-threads = N-CPU * U-CPU * (1 + W/C)其中:
- N-CPU是處理器的核的數目,可以通過Runtime.getRuntime().availableProcessors()得到
- U-CPU是期望的CPU利用率(該值應該介于0和1之間)
- W/C是等待時間與計算時間的比率
你的應用99%的時間都在等待商店的響應,所以估算出的W/C比率為1/100。這意味著如果你期望的CPU利用率是100%,你需要創建一個擁有400個線程的線程池。
實際操作中,如果你創建的線程數比商店的數目更多,反而是一種浪費,因為這樣做之后,你線程池中的有些線程根本沒有機會被使用。出于這種考慮,我們建議你將執行器使用的線程數,與你需要查詢的商店數目設定為同一個值,這樣每個商店都應該對應一個服務線程。
不過,為了避免發生由于商店的數目過多導致服務器超負荷而崩潰,你還是需要設置一個上限,比如100個線程.
private final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100),new ThreadFactory() {public Thread newThread(Runnable r) {Thread t = new Thread(r);t.setDaemon(true);return t;}});注意,你現在正創建的是一個由守護線程構成的線程池。Java程序無法終止或者退出一個正在運行中的線程,所以最后剩下的那個線程會由于一直等待無法發生的事件而引發問題。
與此相反,如果將線程標記為守護進程,意味著程序退出時它也會被回收。這二者之間沒有性能上的差異?,F在,你可以將執行器作為第二個參數傳遞給supplyAsync工廠方法了。
CompletableFuture.supplyAsync(() -> shop.getName() + " price is " + shop.getPrice(product), executor);處理需大量使用異步操作的情況時,利用CompletableFutures向其提交任務執行幾乎是最有效的策略。
并行——使用Stream還是CompletableFutures?
目前為止,你已經知道對集合進行并行計算有兩種方式:
后者提供了更多的靈活性,你可以調整線程池的大小,而這能幫助你確保整體的計算不會因為線程都在等待I/O而發生阻塞。
我們對使用這些API的建議如下。
-
如果你進行的是計算密集型的操作,并且沒有I/O,那么推薦使用Stream接口,因為實現簡單,同時效率也可能是最高的(如果所有的線程都是計算密集型的,那就沒有必要創建比處理器核數更多的線程)。
-
反之,如果你并行的工作單元還涉及等待I/O的操作(包括網絡連接等待),那么使用CompletableFuture靈活性更好,你可以像前文討論的那樣,依據等待/計算,或者W/C的比率設定需要使用的線程數。這種情況不使用并行流的另一個原因是,處理流的流水線中如果發生I/O等待,流的延遲特性會讓我們很難判斷到底什么時候觸發了等待。
對多個異步任務進行流水線操作
BestPriceFinder2
BestPriceFinderMain2
讓我們假設所有的商店都同意使用一個集中式的折扣服務。
Discount
實現折扣服務
Quote
使用Discount服務
由于Discount服務是一種遠程服務,你還需要增加1秒鐘的模擬延遲。
以最簡單的、順序的而且同步執行的實現。
public List<String> findPricesSequential(String product) {return shops.stream().map(shop -> shop.getPrice2(product)).map(Quote::parse).map(Discount::applyDiscount).collect(Collectors.toList()); }- 第一個操作將每個shop對象轉換成了一個字符串,該字符串包含了該 shop中指定商品的價格和折扣代碼。
- 第二個操作對這些字符串進行了解析,在Quote對象中對它們進行轉換。
- 最終,第三個map會操作聯系遠程的Discount服務,計算出最終的折扣價格,并返回該價格及提供該價格商品的shop。
這種實現方式的性能遠非最優,只是運行基準測試。
把流轉換為并行流的方式,非常容易提升該程序的性能。
Stream底層依賴的是線程數量固定的通用線程池,擴展性差。相反,你也知道,如果自定義CompletableFutures調度任務執行的執行器能夠更充分地利用CPU資源
構造同步和異步操作
List<String> findPrices(String product) {List<CompletableFuture<String>> priceFutures = shops.stream().map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor)).map(future -> future.thenApply(Quote::parse)).map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor))).collect(toList());return priceFutures.stream().map(CompletableFuture::join).collect(toList()); }三次轉換流程圖
運行結果
[BestPrice price is 204.78, LetsSaveBig price is 190.85, MyFavoriteShop price is 128.92, BuyItAll price is 140.31, ShopEasy price is 166.1] composed CompletableFuture done in 2025 msecs將兩個CompletableFuture對象整合起來,無論它們是否存在依賴
CompletableFuture<Double> futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product)).thenCombine(CompletableFuture.supplyAsync(() -> ExchangeService.getRate(Money.EUR, Money.USD)),(price, rate) -> price * rate);對Future和CompletableFuture的回顧
前兩節非常清晰地呈現了相對于采用Java 8 之前提供的Future 實現, CompletableFuture 版本實現所具備的巨大優勢。
CompletableFuture利用Lambda表達式以聲明式的API提供了一種機制,能夠用最有效的方式,非常容易地將多個以同步或異步方式執行復雜操作的任務結合到一起。
為了更直觀地感受一下使用CompletableFuture在代碼可讀性上帶來的巨大提升,嘗試僅使用Java 7中提供的特性。
利用Java 7的方法合并兩個Future對象,實現上一節例子。
ExecutorService executor = Executors.newCachedThreadPool(); List<Future<Double>> priceFutures = new ArrayList<>(); for (Shop shop : shops) {final Future<Double> futureRate = executor.submit(new Callable<Double>() { public Double call() {return ExchangeService.getRate(Money.EUR, Money.USD);}});Future<Double> futurePriceInUSD = executor.submit(new Callable<Double>() { public Double call() {try {double priceInEUR = shop.getPrice(product);return priceInEUR * futureRate.get();} catch (InterruptedException | ExecutionException e) {throw new RuntimeException(e.getMessage(), e);}}});priceFutures.add(futurePriceInUSD); }即時響應——CompletableFuture的completion事件
希望盡快將不同商店中的商品價格呈現給你的用戶(這是車輛保險或者機票比價網站的典型需求),而不是像之前那樣,等所有的數據都完備之后再呈現
你希望的效果是,只要有商店返回商品價格就在第一時間顯示返回值,不再等待那些還未返回的商店(有些甚至會發生超時)
一個模擬生成0.5秒至2.5秒隨機延遲的方法
private static final Random random = new Random();public static void randomDelay() {int delay = 500 + random.nextInt(2000);try {Thread.sleep(delay);} catch (InterruptedException e) {throw new RuntimeException(e);} }對最佳價格查詢器應用的優化
重構findPrices方法返回一個由Future構成的流
public Stream<CompletableFuture<String>> findPricesStream(String product) {return shops.stream().map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice2(product), executor)).map(future -> future.thenApply(Quote::parse)).map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor))); }Java 8 的CompletableFuture 的thenAccept 方法用來接收CompletableFuture執行完畢后的返回值做參數。
findPricesStream("myPhone").map(f -> f.thenAccept(System.out::println));thenCompose和thenCombine方法一樣,thenAccept方法也提供了一個異步版本,名為thenAcceptAsync。異步版本的方法會對處理結果的消費者進行調度,從線程池中選擇一個新的線程繼續執行,不再由同一個線程完成CompletableFuture的所有任務。
因為你想要避免不必要的上下文切換,更重要的是你希望避免在等待線程上浪費時間,盡快響應CompletableFuture的completion事件,所以這里沒有采用異步版本。
由于thenAccept 方法已經定義了如何處理CompletableFuture 返回的結果,一旦CompletableFuture計算得到結果,它就返回一個CompletableFuture。所以,map操作返回的是一個Stream<CompletableFuture>。
對這個<CompletableFuture>對象,你能做的事非常有限,只能等待其運行結束,不過這也是你所期望的。你還希望能給最慢的商店一些機會,讓它有機會打印輸出返回的價格。為了實現這一目的,你可以把構成Stream的所有CompletableFuture對象放到一個數組中,等待所有的任務執行完成,
響應CompletableFuture的completion事件
CompletableFuture[] futures = findPricesStream("myPhone").map(f -> f.thenAccept(System.out::println)).toArray(size -> new CompletableFuture[size]); CompletableFuture.allOf(futures).join();allOf工廠方法接收一個由CompletableFuture構成的數組,數組中的所有CompletableFuture對象執行完成之后,它返回一個CompletableFuture對象。這意味著,如果你需要等待最初Stream 中的所有CompletableFuture 對象執行完畢, 對allOf 方法返回的CompletableFuture執行join操作是個不錯的主意。
這個方法對“最佳價格查詢器”應用也是有用的,因為你的用戶可能會困惑是否后面還有一些價格沒有返回,使用這個方法,你可以在執行完畢之后打印輸出一條消息“All shops returned results or timed out”。
然而在另一些場景中,你可能希望只要CompletableFuture對象數組中有任何一個執行完畢就不再等待,比如,你正在查詢兩個匯率服務器,任何一個返回了結果都能滿足你的需求。在這種情況下,你可以使用一個類似的工廠方法anyOf。該方法接收一個CompletableFuture對象構成的數組,返回由第一個執行完畢的CompletableFuture對象的返回值構成的CompletableFuture。
付諸實踐
執行這段代碼你會看到不同商店的價格不再像之前那樣總是在一個時刻返回,而是隨著商店折扣價格返回的順序逐一地打印輸出
public void printPricesStream(String product) {long start = System.nanoTime();CompletableFuture[] futures = findPricesStream(product).map(f -> f.thenAccept(s -> System.out.println(s + " (done in " + ((System.nanoTime() - start) / 1_000_000) + " msecs)"))).toArray(size -> new CompletableFuture[size]);CompletableFuture.allOf(futures).join();System.out.println("All shops have now responded in " + ((System.nanoTime() - start) / 1_000_000) + " msecs"); }運行結果
BestPrice price is 127.88 (done in 2022 msecs) LetsSaveBig price is 147.21 (done in 2024 msecs) ShopEasy price is 224.23 (done in 2025 msecs) MyFavoriteShop price is 119.11 (done in 2025 msecs) BuyItAll price is 111.53 (done in 2025 msecs) All shops have now responded in 2026 msecs小結
- 執行比較耗時的操作時,尤其是那些依賴一個或多個遠程服務的操作,使用異步任務可以改善程序的性能,加快程序的響應速度。
- 你應該盡可能地為客戶提供異步API。使用CompletableFuture類提供的特性,你能夠輕松地實現這一目標。
- CompletableFuture類還提供了異常管理的機制,讓你有機會拋出/管理異步任務執行中發生的異常。
- 將同步API的調用封裝到一個CompletableFuture中,你能夠以異步的方式使用其結果。
- 如果異步任務之間相互獨立,或者它們之間某一些的結果是另一些的輸入,你可以將這些異步任務構造或者合并成一個。
- 你可以為CompletableFuture注冊一個回調函數,在Future執行完畢或者它們計算的結果可用時,針對性地執行一些程序。
- 你可以決定在什么時候結束程序的運行,是等待由CompletableFuture對象構成的列
總結
以上是生活随笔為你收集整理的《Java8实战》笔记(11):CompletableFuture-组合式异步编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法(18)-leetcode-剑指of
- 下一篇: 机器学习知识总结系列- 特征工程(1-1