Java8主要的語言增強的能力有:
(1)lambda表達式
(2)stream式操作
(3)CompletableFuture
其中第三個特性,就是今天我們想要聊的話題,正是因爲CompletableFuture的出現,才使得使用Java進行異步編程提供了可能。
什麼是CompletableFuture?
CompletableFuture在Java裏面被用於異步編程,異步通常意味着非阻塞,可以使得我們的任務單獨運行在與主線程分離的其他線程中,並且通過 回調可以在主線程中得到異步任務的執行狀態,是否完成,和是否異常等信息。CompletableFuture實現了Future, CompletionStage接口,實現了Future接口就可以兼容現在有線程池框架,而CompletionStage接口才是異步編程的接口抽象,裏面定義多種異步方法,通過這兩者集合,從而打造出了強大的CompletableFuture類。
Future vs CompletableFuture
Futrue在Java裏面,通常用來表示一個異步任務的引用,比如我們將任務提交到線程池裏面,然後我們會得到一個Futrue,在Future裏面有isDone方法來 判斷任務是否處理結束,還有get方法可以一直阻塞直到任務結束然後獲取結果,但整體來說這種方式,還是同步的,因爲需要客戶端不斷阻塞等待或者不斷輪詢才能知道任務是否完成。
Future的主要缺點如下:
(1)不支持手動完成
這個意思指的是,我提交了一個任務,但是執行太慢了,我通過其他路徑已經獲取到了任務結果,現在沒法把這個任務結果,通知到正在執行的線程,所以必須主動取消或者一直等待它執行完成。
(2)不支持進一步的非阻塞調用
這個指的是我們通過Future的get方法會一直阻塞到任務完成,但是我還想在獲取任務之後,執行額外的任務,因爲Future不支持回調函數,所以無法實現這個功能。
(3)不支持鏈式調用
這個指的是對於Future的執行結果,我們想繼續傳到下一個Future處理使用,從而形成一個鏈式的pipline調用,這在Future中是沒法實現的。
(4)不支持多個Future合併
比如我們有10個Future並行執行,我們想在所有的Future運行完畢之後,執行某些函數,是沒法通過Future實現的。
(5)不支持異常處理
Future的API沒有任何的異常處理的api,所以在異步運行時,如果出了問題是不好定位的。
簡單的使用CompletableFuture
1,先看一個最簡單的例子
在主線程裏面創建一個CompletableFuture,然後主線程調用get方法會阻塞,最後我們在一個子線程中 使其終止。
CompletableFuture<String> completableFuture=new CompletableFuture<String>();
Runnable runnable=new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(getThreadName()+"執行.....");
completableFuture.complete("success");//在子線程中完成主線程completableFuture的完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1=new Thread(runnable);
t1.start();//啓動子線程
String result=completableFuture.get();//主線程阻塞,等待完成
System.out.println(getThreadName()+"1x: "+result);
輸出結果:
Thread-0線程=> 執行..... main線程=> 1x: success
2,運行一個簡單的沒有返回值的異步任務
CompletableFuture<Void> future=CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
try {
System.out.println(getThreadName()+"正在執行一個沒有返回值的異步任務。");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
future.get();
System.out.println(getThreadName()+" 結束。");
輸出如下:
ForkJoinPool.commonPool-worker-1線程=> 正在執行一個沒有返回值的異步任務。 main線程=> 結束。
從上面我們可以看到CompletableFuture默認運行使用的是ForkJoin的的線程池。當然,你也可以用lambda表達式使得代碼更精簡。
3,運行一個有返回值的異步任務
CompletableFuture<String> future=CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
System.out.println(getThreadName()+"正在執行一個有返回值的異步任務。");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "OK";
}
});
String result=future.get();
System.out.println(getThreadName()+" 結果:"+result);
輸出結果:
ForkJoinPool.commonPool-worker-1線程=> 正在執行一個有返回值的異步任務。 main線程=> 結果:OK
當然,上面默認的都是ForkJoinPool我們也可以換成Executor相關的Pool,其api都有支持如下:
static CompletableFuture<Void> runAsync(Runnable runnable)
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
高級的使用CompletableFuture
前面提到的幾種使用方法是使用異步編程最簡單的步驟,CompletableFuture.get()的方法會阻塞直到任務完成,這其實還是同步的概念,這對於一個異步系統是不夠的,因爲真正的異步是需要支持回調函數,這樣以來,我們就可以直接在某個任務幹完之後,接着執行回調裏面的函數,從而做到真正的異步概念。
在CompletableFuture裏面,我們通過thenApply(), thenAccept(),thenRun()方法,來運行一個回調函數。
(1)thenApply()
這個方法,其實用過函數式編程的人非常容易理解,類似於scala和spark的map算子,通過這個方法可以進行多次鏈式轉化並返回最終的加工結果。
看下面一個例子:
public static void asyncCallback() throws ExecutionException, InterruptedException {
CompletableFuture<String> task=CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
System.out.println(getThreadName()+"supplyAsync");
return "123";
}
});
CompletableFuture<Integer> result1 = task.thenApply(number->{
System.out.println(getThreadName()+"thenApply1");
return Integer.parseInt(number);
});
CompletableFuture<Integer> result2 = result1.thenApply(number->{
System.out.println(getThreadName()+"thenApply2");
return number*2;
});
System.out.println(getThreadName()+" => "+result2.get());
}
輸出結果:
ForkJoinPool.commonPool-worker-1線程=> supplyAsync main線程=> thenApply1 main線程=> thenApply2 main線程=> => 246
(2)thenAccept()
這個方法,可以接受Futrue的一個返回值,但是本身不在返回任何值,適合用於多個callback函數的最後一步操作使用。
例子如下:
public static void asyncCallback2() throws ExecutionException, InterruptedException {
CompletableFuture<String> task=CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
System.out.println(getThreadName()+"supplyAsync");
return "123";
}
});
CompletableFuture<Integer> chain1 = task.thenApply(number->{
System.out.println(getThreadName()+"thenApply1");
return Integer.parseInt(number);
});
CompletableFuture<Integer> chain2 = chain1.thenApply(number->{
System.out.println(getThreadName()+"thenApply2");
return number*2;
});
CompletableFuture<Void> result=chain2.thenAccept(product->{
System.out.println(getThreadName()+"thenAccept="+product);
});
result.get();
System.out.println(getThreadName()+"end");
}
結果如下:
ForkJoinPool.commonPool-worker-1線程=> supplyAsync main線程=> thenApply1 main線程=> thenApply2 main線程=> thenAccept=246 main線程=> end
(3) thenRun()
這個方法與上一個方法類似,一般也用於回調函數最後的執行,但這個方法不接受回調函數的返回值,純粹就代表執行任務的最後一個步驟:
public static void asyncCallback3() throws ExecutionException, InterruptedException {
CompletableFuture.supplyAsync(()->{
System.out.println(getThreadName()+"supplyAsync: 一階段任務");
return null;
}).thenRun(()->{
System.out.println(getThreadName()+"thenRun: 收尾任務");
}).get();
}
結果:
ForkJoinPool.commonPool-worker-1線程=> supplyAsync: 一階段任務 main線程=> thenRun: 收尾任務
這裏注意,截止到目前,前面的例子代碼只會涉及兩個線程,一個是主線程一個是ForkJoinPool池的線程,但其實上面的每一步都是支持異步運行的,其api如下:
// thenApply() variants <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
我們看下改造後的一個例子:
public static void asyncCallback4() throws ExecutionException, InterruptedException {
CompletableFuture<String> ref1= CompletableFuture.supplyAsync(()->{
try {
System.out.println(getThreadName()+"supplyAsync開始執行任務1.... ");
// TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(getThreadName()+"supplyAsync: 任務1");
return null;
});
CompletableFuture<String> ref2= CompletableFuture.supplyAsync(()->{
try {
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(getThreadName()+"thenApplyAsync: 任務2");
return null;
});
CompletableFuture<String> ref3=ref2.thenApplyAsync(value->{
System.out.println(getThreadName()+"thenApplyAsync: 任務2的子任務");
return null;
});
Thread.sleep(4000);
System.out.println(getThreadName()+ref3.get());
}
輸出結果如下:
ForkJoinPool.commonPool-worker-1線程=> supplyAsync開始執行任務1.... ForkJoinPool.commonPool-worker-1線程=> supplyAsync: 任務1 ForkJoinPool.commonPool-worker-1線程=> supplyAsync: 任務2 ForkJoinPool.commonPool-worker-2線程=> thenApplyAsync: 任務2的子任務 main線程=> null
我們可以看到,ForkJoin池的線程1,執行了前面的三個任務,但是第二個任務的子任務,因爲我們了使用也異步提交所以它用的線程是ForkJoin池的線程2,最終由於main線程處執行了get是最後結束的。
還有一點需要注意:
ForkJoinPool所有的工作線程都是守護模式的,也就是說如果主線程退出,那麼整個處理任務都會結束,而不管你當前的任務是否執行完。如果需要主線程等待結束,可採用ExecutorsThreadPool,如下:
ExecutorService pool = Executors.newFixedThreadPool(5); final CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { ... }, pool);
(4)thenCompose合併兩個有依賴關係的CompletableFutures的執行結果
CompletableFutures在執行兩個依賴的任務合併時,會返回一個嵌套的結果列表,爲了避免這種情況我們可以使用thenCompose來返回,直接獲取最頂層的結果數據即可:
public static void asyncCompose() throws ExecutionException, InterruptedException {
CompletableFuture<String> future1=CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
return "1";
}
});
CompletableFuture<String>nestedResult = future1.thenCompose(value->
CompletableFuture.supplyAsync(()->{
return value+"2";
}));
System.out.println(nestedResult.get());
}
(5)thenCombine合併兩個沒有依賴關係的CompletableFutures任務
CompletableFuture<Double> d1= CompletableFuture.supplyAsync(new Supplier<Double>() {
@Override
public Double get() {
return 1d;
}
});
CompletableFuture<Double> d2= CompletableFuture.supplyAsync(new Supplier<Double>() {
@Override
public Double get() {
return 2d;
}
});
CompletableFuture<Double> result= d1.thenCombine(d2,(number1,number2)->{
return number1+number2;
});
System.out.println(result.get());
(6)合併多個任務的結果allOf與anyOf
上面說的是兩個任務的合併,那麼多個任務需要使用allOf或者anyOf方法。
allOf適用於,你有一系列獨立的future任務,你想等其所有的任務執行完後做一些事情。舉個例子,比如我想下載100個網頁,傳統的串行,性能肯定不行,這裏我們採用異步模式,同時對100個網頁進行下載,當所有的任務下載完成之後,我們想判斷每個網頁是否包含某個關鍵詞。
下面我們通過隨機數來模擬上面的這個場景如下:
public static void mutilTaskTest() throws ExecutionException, InterruptedException {
//添加n個任務
CompletableFuture<Double> array[]=new CompletableFuture[3];
for ( int i = 0; i < 3; i++) {
array[i]=CompletableFuture.supplyAsync(new Supplier<Double>() {
@Override
public Double get() {
return Math.random();
}
});
}
//獲取結果的方式一
// CompletableFuture.allOf(array).get();
// for(CompletableFuture<Double> cf:array){
// if(cf.get()>0.6){
// System.out.println(cf.get());
// }
// }
//獲取結果的方式二,過濾大於指定數字,在收集輸出
List<Double> rs= Stream.of(array).map(CompletableFuture::join).filter(number->number>0.6).collect(Collectors.toList());
System.out.println(rs);
}
結果如下:
[0.8228784717152199]
注意其中的join方法和get方法類似,僅僅在於在Future不能正常完成的時候拋出一個unchecked的exception,這可以確保它用在Stream的map方法中,直接使用get是沒法在map裏面運行的。
anyOf方法,也比較簡單,意思就是隻要在多個future裏面有一個返回,整個任務就可以結束,而不需要等到每一個future結束。
CompletableFuture<String> f1=CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "wait 4 seconds";
}
});
CompletableFuture<String> f2=CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "wait 2 seconds";
}
});
CompletableFuture<String> f3=CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "wait 10 seconds";
}
});
CompletableFuture<Object> result= CompletableFuture.anyOf(f1,f2,f3);
System.out.println(result.get());
輸出結果:
wait 2 seconds
注意由於Anyof返回的是其中任意一個Future所以這裏沒有明確的返回類型,統一使用Object接受,留給使用端處理。
(7)exceptionally異常處理
異常處理是異步計算的一個重要環節,下面看看如何在CompletableFuture中使用:
int age=-1;
CompletableFuture<String> task= CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
if(age<0){
throw new IllegalArgumentException("性別必須大於0");
}
if(age<18){
return "未成年人";
}
return "成年人";
}
}).exceptionally(ex->{
System.out.println(ex.getMessage());
return "發生 異常"+ex.getMessage();
});
System.out.println(task.get());
結果如下:
java.lang.IllegalArgumentException: 性別必須大於0 發生 異常java.lang.IllegalArgumentException: 性別必須大於0
此外還有另外一種異常捕捉方法handle,無論發生異常都會執行,示例如下:
int age=10;
CompletableFuture<String> task= CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
if(age<0){
throw new IllegalArgumentException("性別必須大於0");
}
if(age<18){
return "未成年人";
}
return "成年人";
}
}).handle((res,ex)->{
System.out.println("執行handle");
if(ex!=null){
System.out.println("發生異常");
return "發生 異常"+ex.getMessage();
}
return res;
});
System.out.println(task.get());
輸出結果:
執行handle 發生異常 發生 異常java.lang.IllegalArgumentException: 性別必須大於0
注意上面的方法如果正常執行,也會執行handle方法。
JDK9 CompletableFuture 類增強的主要內容
(1)支持對異步方法的超時調用
orTimeout()
completeOnTimeout()
(2)支持延遲調用
Executor delayedExecutor(long delay, TimeUnit unit, Executor executor)
Executor delayedExecutor(long delay, TimeUnit unit)
詳細內容,可以參考Oracle官網文檔,這裏不再過多介紹。
總結:
本文主要介紹了CompletableFuture的定義,概念及在Java中使用的例子,通過CompletableFuture我們可以實現異步編程的能力,從而使得我們開發的任務可以擁有更強大的能力。