當拿到CompletableFuture的時候,是真的不知道該怎麼去講算作能夠講清楚,個人覺得COmpletableFuture自己通過部分的內部類+執行器+線程池+函數式,就維護了一個龐大的執行系統,那麼這個龐大的系統能夠異步的幫助我們很好的優化我們的程序執行效率
CompletableFuture:
CompletableFuture實現了兩個接口,CompletableStage、Future,所依其功能就是能夠異步執行的線程中間態,這其中的每個中間態都可以相互串聯起來,達到最後完成一個任務的作用
Future之前有簡單講過,可以代表的是一種異步執行;而CompletableStage是一種中間狀態,返回的就是它自己,這樣一來就可以將很多箇中間狀態串聯起來,或者並行執行,得到最終結果,並提升效率
-
創建CompletableFuture對象
創建有三種方式- 通過new
CompletableFuture d = new CompletableFuture();
- 通過靜態輔助方法
用來返回一個已經計算好的值(計算好的值,是通過參數傳入),相當於是一個同步的方法,其實底層就是調用了new CompletableFuture(U u)
public static <U> CompletableFuture<U> completedFuture(U value)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier); public static CompletableFuture<Void> runAsync(Runnable runnable);
傳入一個Supplier類型(格式如下:()-> reture U),最後返回一個值爲U的CompletabeFuture,
其實底層是通過Executor的execute(Runnable runnable), - 通過new
那麼問題來了:爲什麼這裏又是Runnable呢?不是說好的會有返回值嘛,怎麼不是Callable?
首先我們來看一下類圖
值得關注的是
一般執行CompletableFuture,不論是創建的(除了直接new以外),還是中間操作都有三個重載方法,其中一個同步執行方法,兩個異步執行方法,兩個異步執行方法中有一個會指定Executor,那如果指定了Executor的話,那麼就會用指定的線程池操作,否則就是用默認的ForkJoinPool中的線程來執行。代碼如下
public CompletableFuture<T> supplyAsync(Supplier<U> supplier)
public CompletableFuture<T> supplyAsync(Supplier<U> supplier)
public CompletableFuture<T> supplyAsync(Supplier<U> supplier, Executor executor)
在後面所有的代碼摘取中一般只摘取其中一個來講解
爲什麼是runnable?
我們來看這麼一條代碼:
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
static <U> CompletableFuture<U> asyncSupplyStage(Executor e,
Supplier<U> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<U> d = new CompletableFuture<U>();
e.execute(new AsyncSupply<U>(d, f));
return d;
}
大部分的中間狀態操作都是這樣的執行方式,會傳入一個Executor,然後通過Executor 來執行一個內部類封裝的的參數Supplier
,然後將結果傳遞給一個新的CompletableFuture
來返回
在CompletableFuture內部創建了一個靜態內部類AsyncSupply, 它相當於是一個適配器,它實現了Runnable,
並且內部聚合了一個CompletableFuture,在實現的run方法中將Supplier執行的結果交給了聚合的CompletableFuture的completeValue()方法,
completeValue()的實現是通過CAS的方式實現的,所以它的操作是線程安全的,最後返回的CompletableFuture,其實就是AsynSupply中聚合的CompletableFutur
runAsync(Runnable) 的執行流程就和supplyAsync的執行流程其實是一樣的,CompletableFuture的內部封裝了一個AsyncRun功能類似於AsyncSupply
- 獲取元素
主動結束操作,這個地方的結束操作和Stream中的終止操作不一樣的是,函數式編程中的終止操作,就是來執行定義的Stream的,這裏的結束操作是指同步的去獲取CompletableFuture最後執行的結果
public T get();
public T get(long timeout, TimeUnit unit);
//嘗試獲取值,如果值還沒有計算出來就返回valueIfAbsent作爲默認值
public T getNow(T valueIfAbsent) ;
//獲取值,用法類似於get()
public T join()
- 計算結果
基本上這所有的中間操作都會傳入一個action,這個action就是一個函數式接口,所以我們傳入的參數只要滿足相應的函數式接口即可
比如以下的
public CompletableFuture<T> whenComplete( BiConsumer<? super T, ? super Throwable> action) {
那麼我們使用的時候傳入的就是兩個參數,參考如下,更多的使用需要參考java8函數式編程新特性
String join = CompletableFuture
//首先創建一個CompletableFuture
.supplyAsync(()->"method")
//當前面一個COmpletableFuture執行完成之後進行的相關操作
.whenComplete((x, y) -> System.out.println(x.substring(1)))
//當前面執行完成之後獲取左後返回的泛型類型中的值
.join();
類似,handle的操作也是屬於中間操作,只不過,whenComplete的操作時傳入的T 類型的BiConsumer,最後返回的是T類型的CompletableFuture,但是handle傳入的是T 類型的BiFunction,所以最後返回的是 U類型的CompletableFuture,相當於是計算結果和轉換兩者的結合體
public <U> CompletableFuture<U> handle(BiFunction<? super T,Throwable,? extends U> fn)
這裏有一點小小的使用經驗,就是看傳入的參數action 是什麼類型,不同類型的Action 就會有不同的行爲,比方說以下的,傳入的是Function,就是將T類型的CompletableFuture 轉化成U類型的CompletableFuture,Consumer就是純消費型的,BiConsumer就是會有兩個參數也是純消費型的,返回類型爲void
//轉換的操作
public <U> CompletableFuture<U> thenApply(
Function<? super T,? extends U> fn)
//純消費操作
public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
//消費兩個Stage
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,
BiConsumer<? super T, ? super U> action)
//消費一個線程
public CompletableFuture<Void> thenRun(Runnable action)
//組合操作
public <U> CompletableFuture<U> thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
//同樣也是組合操作
public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
最後以《java 8 in action》中對於CompletableFuture的操作案例做爲總結
/**
* 這裏咱們可以聯想到,在我們真實項目中,某個集合中的所有元素都需要在請求一下另一個系統的數據,而另一個系統的數據響應時間很慢,
* 如果程序是串行的,那麼效率就很低下了,這個時候就是我們開始需要優化程序的時候了
* @param shops 這裏很有意思的一個點事當我們傳入的shops數組的大小不同,線程池使用線程數量不同最後這個程序跑粗來的效果是不一樣的
* @param product
* @return
*/
public static List<Integer> findPrices5(List<Shop> shops, String product) {
ExecutorService executor = Executors.newFixedThreadPool(8);
List<CompletableFuture<Integer>> priceFutures = shops.stream().map(shop -> CompletableFuture.supplyAsync(() ->
String.format("%s price is %.2f",
shop.getShopName(), shop.getPrice(product)), executor)
).map(future -> future.thenApply(String::hashCode)).collect(toList());
executor.shutdown();
return priceFutures.stream().map(CompletableFuture::join).collect(toList());
}
public double getPrice(String produce) {
return calculatePrice(produce);
}
private double calculatePrice(String product) {
delay();
return random.nextDouble() * product.charAt(0) + product.charAt(1);
}
//模擬比較耗時的線程操作
public static void delay() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void run() {
List<Shop> shops = new ArrayList<>(225);
for (int i = 0; i < 4; i++) {
shops.add(new Shop("BuyItDDal"));
}
long start = System.nanoTime();
System.out.println(findPrices5(shops,"myPhone27S"));
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("Done in " + duration + " msecs");
}