Java8 - 避免代码阻塞的骚操作
文章目錄
- Pre
- 避免同步阻塞的困擾
- V1.0 改進 -采用Stream 順序查詢 (不理想)
- V2.0 改進 - 使用并行流對請求進行并行操作 (good)
- V3.0 改進 - 使用 CompletableFuture發起異步請求 ()
- 更好的方案
Pre
Java8 - 使用工廠方法 supplyAsync創建 CompletableFuture
接著上面的例子
假設非常不幸,無法控制 Shop 類提供API的具體實現,最終提供給你的API都是同步阻塞式的方法。這也是當你試圖使用服務提供的HTTP API時最常發生的情況。你會學到如何以異步的方式查詢多個商店,避免被單一的請求所阻塞,并由此提升你的“最佳價格查詢器”的性能和吞吐量。
避免同步阻塞的困擾
假設你需要查詢的所有商店只提供了同步API,換句話說,你有一個商家的列表,如下所示:
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),new Shop("LetsSaveBig"),new Shop("MyFavoriteShop"),new Shop("BuyItAll"));你需要使用下面這樣的簽名實現一個方法,它接受產品名作為參數,返回一個字符串列表,這個字符串列表中包括商店的名稱、該商店中指定商品的價格:
public List<String> findPrices(String product);V1.0 改進 -采用Stream 順序查詢 (不理想)
第一個想法可能是使用 Stream 特性。
【采用順序查詢所有商店的方式實現的 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正如你預期的, findPrices 方法的執行時間4S+,因為對這4個商店的查詢是順序進行的,并且一個查詢操作會阻塞另一個,每一個操作都要花費大于1S的時間計算請求商品的價格。
怎樣才能改進這個結果呢?
V2.0 改進 - 使用并行流對請求進行并行操作 (good)
對V1.0改成并行試試?
【對 findPrices 進行并行操作】
public List<String> findPrices(String product) { return shops.parallelStream().map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product))).collect(toList()); }區別在于 parallelStream ,使用并行流并行流從不同的商店獲取價格。
運行代碼,與V·1.0的執行結果相比較,發現了新版 findPrices 的改進了吧。
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price is 214.13, BuyItAll price is 184.74] Done in 1180 msecs相當不錯啊!看起來這是個簡單但有效的主意:現在對四個不同商店的查詢實現了并行,所以完成所有操作的總耗時只有1S多一點兒。
還能能做得更好嗎? 要不試試CompletableFuture ,將 findPrices 方法中對不同商店的同步調用替換為異步調用。
V3.0 改進 - 使用 CompletableFuture發起異步請求 ()
我們可以使用工廠方法 supplyAsync 創建 CompletableFuture 對象。讓我們把它利用起來:
List<CompletableFuture<String>> priceFutures =shops.stream().map(shop -> CompletableFuture.supplyAsync(() -> String.format("%s price is %.2f",shop.getName(), shop.getPrice(product)))).collect(toList());使用這種方式,你會得到一個 List<CompletableFuture<String>> ,列表中的每個CompletableFuture 對象在計算完成后都包含商店的 String 類型的名稱。但是,由于你用CompletableFutures 實現的 findPrices 方法要求返回一個 List<String> ,你需要等待所有的 future 執行完畢,將其包含的值抽取出來,填充到列表中才能返回
為了實現這個效果,你可以向最初的 List<CompletableFuture<String>> 添加第二個map 操作,對 List 中的所有 future 對象執行 join 操作,一個接一個地等待它們運行結束。
Note: CompletableFuture 類中的 join 方法和 Future 接口中的 get 有相同的含義,并且也聲明在Future 接口中,它們唯一的不同是 join 不會拋出任何檢測到的異常。使用它你不再需要使用try / catch 語句塊讓你傳遞給第二個 map 方法的Lambda表達式變得過于臃腫。
所有這些整合在一起,你就可以重新實現 findPrices 了,具體代碼如下
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()); }注意到了嗎?這里使用了兩個不同的 Stream 流水線,而不是在同一個處理流的流水線上一個接一個地放兩個 map 操作——這其實是有緣由的。
考慮流操作之間的延遲特性,如果你在單一流水線中處理流,發向不同商家的請求只能以同步、順序執行的方式才會成功。因此,每個創建 CompletableFuture 對象只能在前一個操作結束之后執行查詢指定商家的動作、通知 join方法返回計算結果。
【為什么 Stream 的延遲特性會引起順序執行,以及如何避免】見下圖
上半部分展示了使用單一流水線處理流的過程,我們看到,執行的流程(以虛線標識)是順序的。事實上,新的 CompletableFuture 對象只有在前一個操作完全結束之后,才能創建。與此相反,圖的下半部分展示了如何先將 CompletableFutures 對象聚集到一個列表中(即圖中以橢圓表示的部分),讓對象們可以在等待其他對象完成操作之前就能啟動。
運行代碼 第三個版本 findPrices 方法的性能,你會得到下面這幾行輸出:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price is 214.13, BuyItAll price is 184.74] Done in 2005 msecs
超過2S意味著利用 CompletableFuture 實現的版本比剛開始原生順序執行且會發生阻塞的版本快。但是它的用時也差不多是使用并行流的前一個版本的兩倍。尤其是,考慮到從順序執行的版本轉換到并行流的版本只做了非常小的改動,就讓人更加沮喪
與此形成鮮明對比的是,我們為采用 CompletableFutures 完成的新版方法做了大量的工作!
但,這就是全部的真相嗎?這種場景下使用 CompletableFutures 真的是浪費時間嗎?或者我們可能漏了某些重要的東西?
更好的方案
并行流的版本工作得非常好,那是因為它能并行地執行四個任務,所以它幾乎能為每個商家分配一個線程。但是,如果你想要增加第五個商家到商點列表中,讓你的“最佳價格查詢”應用
總結
以上是生活随笔為你收集整理的Java8 - 避免代码阻塞的骚操作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java8 - 使用工厂方法 suppl
- 下一篇: java美元兑换,(Java实现) 美元