概述
為了展示 CompletableFuture 的強(qiáng)大特性, 創(chuàng)建一個(gè)名為 best-price-finder 的應(yīng)用,它會(huì)查詢多個(gè)在線商店,依據(jù)給定的產(chǎn)品或服務(wù)找出最低的價(jià)格。
這個(gè)過(guò)程中,會(huì)學(xué)到幾個(gè)重要的技能。
- 如何提供異步API
- 如何讓你使用了同步API的代碼變?yōu)榉亲枞a
我們將共同學(xué)習(xí)如何使用流水線將兩個(gè)接續(xù)的異步操作合并為一個(gè)異步計(jì)算操作。 比如,在線商店返回了你想要購(gòu)買的商品的原始價(jià)格,并附帶著一個(gè)折扣代碼――最終,要計(jì)算出該商品的實(shí)際價(jià)格,你不得不訪問(wèn)第二個(gè)遠(yuǎn)程折扣服務(wù),查詢?cè)撜劭鄞a對(duì)應(yīng)的折扣比率
- 如何以響應(yīng)式的方式處理異步操作的完成事件,以及隨著各個(gè)商品返回它的商品價(jià)格,最佳價(jià)格查詢器如何持續(xù)的更新每種商品的最佳推薦,而不是等待所有的商店都返回他們各自的價(jià)格(這種方式存在著一定的風(fēng)險(xiǎn),一旦某家商店的服務(wù)中斷,用戶可能遭遇白屏)。
同步API VS 異步API
同步API
是對(duì)傳統(tǒng)方法的另一種稱呼:你調(diào)用了某個(gè)方法,調(diào)用方在被調(diào)用方運(yùn)行的過(guò)程中會(huì)等待,被調(diào)用方運(yùn)行結(jié)束返回,調(diào)用方取的了被調(diào)用方的返回值并繼續(xù)運(yùn)行。
即使調(diào)用方和被調(diào)用方在不同的線程中運(yùn)行,調(diào)用方還是需要等被調(diào)用方結(jié)束運(yùn)行,這就是 阻塞式調(diào)用。
異步API
與同步API相反,異步API會(huì)直接返回,或者至少在被調(diào)用方計(jì)算完成之前,將它剩余的計(jì)算任務(wù)交給另一個(gè)線程去做,該線程和調(diào)用方是異步的。 這就是非阻塞調(diào)用。
執(zhí)行剩余的計(jì)算任務(wù)的線程將他的計(jì)算結(jié)果返回給調(diào)用方。 返回的方式要么通過(guò)回調(diào)函數(shù),要么由調(diào)用方再此執(zhí)行一個(gè)“等待,指導(dǎo)計(jì)算完成”的方法調(diào)用。
同步的困擾
為了實(shí)現(xiàn)最佳價(jià)格查詢器應(yīng)用,讓我們從每個(gè)商店都應(yīng)該提供的API定義入手。
首先,商店應(yīng)該聲明依據(jù)指定產(chǎn)品名稱返回價(jià)格的方法:
public class Shop { public double getPrice(String product) { // TODO } }
該方法的內(nèi)部實(shí)現(xiàn)會(huì)查詢商店的數(shù)據(jù)庫(kù),但也有可能執(zhí)行一些其他耗時(shí)的任務(wù),比如聯(lián)系其他外部服務(wù)。
用 delay 方法模擬這些長(zhǎng)期運(yùn)行的方法的執(zhí)行,模擬執(zhí)行1S ,方法聲明如下。
public static void delay() { try { Thread.sleep(1000L); } catch (InterruptedException e) { throw new RuntimeException(e); } }
getPrice 方法會(huì)調(diào)用 delay 方法,并返回一個(gè)隨機(jī)計(jì)算的值
public double getPrice(String product) { return calculatePrice(product); } private double calculatePrice(String product) { delay(); return random.nextDouble() * product.charAt(0) + product.charAt(1); }
很明顯,這個(gè)API的使用者(這個(gè)例子中為最佳價(jià)格查詢器)調(diào)用該方法時(shí),它依舊會(huì)被阻塞。為等待同步事件完成而等待1S,這是無(wú)法接受的,尤其是考慮到最佳價(jià)格查詢器對(duì)網(wǎng)絡(luò)中的所有商店都要重復(fù)這種操作。
接下來(lái)我們會(huì)了解如何以異步方式使用同步API解決這個(gè)問(wèn)題。但是,出于學(xué)習(xí)如何設(shè)計(jì)異步API的考慮, 你希望以異步API的方式重寫這段代碼, 假裝我們還在深受這一困難的煩惱,如何以異步API的方式重寫這段代碼,讓用戶更流暢地訪問(wèn)呢?
實(shí)現(xiàn)異步API
將同步方法改為異步方法
為了實(shí)現(xiàn)這個(gè)目標(biāo),你首先需要將 getPrice 轉(zhuǎn)換為 getPriceAsync 方法,并修改它的返回值:
public Future<Double> getPriceAsync(String product) { ... }
我們知道 ,Java 5引入了 java.util.concurrent.Future 接口表示一個(gè)異步計(jì)算(即調(diào)用線程可以繼續(xù)運(yùn)行,不會(huì)因?yàn)檎{(diào)用方法而阻塞)的結(jié)果 。
這意味著 Future 是一個(gè)暫時(shí)還不可知值的處理器,這個(gè)值在計(jì)算完成后,可以通過(guò)調(diào)用它的 get 方法取得。因?yàn)檫@樣的設(shè)計(jì), getPriceAsync 方法才能立刻返回,給調(diào)用線程一個(gè)機(jī)會(huì),能在同一時(shí)間去執(zhí)行其他有價(jià)值的計(jì)算任務(wù)。
新的 CompletableFuture 類提供了大量的方法,讓我們有機(jī)會(huì)以多種可能的方式輕松地實(shí)現(xiàn)這個(gè)方法,比如下面就是這樣一段實(shí)現(xiàn)代碼
【getPriceAsync方法的實(shí)現(xiàn)】
在這段代碼中,創(chuàng)建了一個(gè)代表異步計(jì)算的 CompletableFuture 對(duì)象實(shí)例,它在計(jì)算完成時(shí)會(huì)包含計(jì)算的結(jié)果。
接著,調(diào)用 fork 創(chuàng)建了另一個(gè)線程去執(zhí)行實(shí)際的價(jià)格計(jì)算工作,不等該耗時(shí)計(jì)算任務(wù)結(jié)束,直接返回一個(gè) Future 實(shí)例。
當(dāng)請(qǐng)求的產(chǎn)品價(jià)格最終計(jì)算得出時(shí),你可以使用它的 complete 方法,結(jié)束completableFuture 對(duì)象的運(yùn)行,并設(shè)置變量的值。
很顯然,這個(gè)新版 Future 的名稱也解釋了它所具有的特性。使用這個(gè)API的客戶端,可以通過(guò)下面的這段代碼對(duì)其進(jìn)行調(diào)用。
【使用異步的API】
我們看到這段代碼中,客戶向商店查詢了某種商品的價(jià)格。由于商?提供了異步API,該次調(diào)用立刻返回了一個(gè) Future 對(duì)象,通過(guò)該對(duì)象客戶可以在將來(lái)的某個(gè)時(shí)刻取得商品的價(jià)格。
這種方式下,客戶在進(jìn)行商品價(jià)格查詢的同時(shí),還能執(zhí)行一些其他的任務(wù),比如查詢其他家商店中商品的價(jià)格,不會(huì)呆呆的阻塞在那里等待第一家商店返回請(qǐng)求的結(jié)果。
最后,如果所有有意義的工作都已經(jīng)完成,客戶所有要執(zhí)行的工作都依賴于商品價(jià)格時(shí),再調(diào)用 Future 的 get 方法。執(zhí)行了這個(gè)操作后,客戶要么獲得 Future 中封裝的值(如果異步任務(wù)已經(jīng)完成),要么發(fā)生阻塞,直到該異步任務(wù)完成,期望的值能夠訪問(wèn)。
輸出
你一定已經(jīng)發(fā)現(xiàn) getPriceAsync 方法的調(diào)用返回遠(yuǎn)遠(yuǎn)早于最終價(jià)格計(jì)算完成的時(shí)間。
我們有可能避免發(fā)生客戶端被住阻塞的風(fēng)險(xiǎn)。實(shí)際上這非常簡(jiǎn)單, Future 執(zhí)行完畢可以發(fā)出一個(gè)通知,僅在計(jì)算結(jié)果可用時(shí)執(zhí)行一個(gè)由Lambda表達(dá)式或者方法引用定義的回
調(diào)函數(shù)。
不過(guò),我們當(dāng)下不會(huì)對(duì)此進(jìn)行討論,現(xiàn)在我們要解決的是另一個(gè)問(wèn)題:如何正確地管理
異步任務(wù)執(zhí)行過(guò)程中可能出現(xiàn)的錯(cuò)誤。
處理異常錯(cuò)誤
如果沒(méi)有意外,我們目前開發(fā)的代碼工作得很正常。但是,如果價(jià)格計(jì)算過(guò)程中產(chǎn)生了錯(cuò)誤會(huì)怎樣呢?非常不幸,這種情況下你會(huì)得到一個(gè)相當(dāng)糟糕的結(jié)果:用于提示錯(cuò)誤的異常會(huì)被限制在試圖計(jì)算商品價(jià)格的當(dāng)前線程的范圍內(nèi),最終會(huì)殺死該線程,而這會(huì)導(dǎo)致等待 get 方法返回結(jié)果的客戶端永久的被阻塞。
客戶端可以使用重載版本的 get 方法,它使用一個(gè)超時(shí)參數(shù)來(lái)避免發(fā)生這樣的情況。這是一種值得推薦的做法,你應(yīng)該盡量在你的代碼中添加超時(shí)判斷斷的邏輯,避免發(fā)生類似的問(wèn)題。
使用這種方法至少能防止程序永遠(yuǎn)的等待下去,超時(shí)發(fā)生時(shí),程序會(huì)得到通知發(fā)生了 Timeout-Exception 。
不過(guò),也因?yàn)槿绱?,你不?huì)有機(jī)會(huì)發(fā)現(xiàn)計(jì)算商品價(jià)格的線程內(nèi)到底發(fā)生了什么問(wèn)題才引發(fā)了這樣的失效。
為了讓客戶端能了解商店無(wú)法提供請(qǐng)求商品價(jià)格的原因,你需要使用
CompletableFuture 的 completeExceptionally 方法將導(dǎo)致 CompletableFuture 內(nèi)發(fā)生問(wèn)題的異常拋出。
代碼如下
【拋出CompletableFuture內(nèi)的異常】
客戶端現(xiàn)在會(huì)收到一個(gè) ExecutionException 異常,該異常接收了一個(gè)包含失敗原因的Exception 參數(shù),即價(jià)格計(jì)算方法最初拋出的異常。
所以,舉例來(lái)說(shuō),如果該方法拋出了一個(gè)運(yùn)行時(shí)異常“product not available”,客戶端就會(huì)得到像下面這樣一段 ExecutionException :
java.util.concurrent.ExecutionException: java.lang.RuntimeException: product
not available at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2237)
at lambdasinaction.chap11.AsyncShopClient.main(AsyncShopClient.java:14)
... 5 more
Caused by: java.lang.RuntimeException: product not available
at lambdasinaction.chap11.AsyncShop.calculatePrice(AsyncShop.java:36)
at lambdasinaction.chap11.AsyncShop.lambda$getPrice$0(AsyncShop.java:23)
at lambdasinaction.chap11.AsyncShop$$Lambda$1/24071475.run(Unknown Source)
at java.lang.Thread.run(Thread.java:744)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持服務(wù)器之家。
原文鏈接:https://artisan.blog.csdn.net/article/details/115450838