CompletableFuture基本使用

引子

在併發編程中,我們經常用到非阻塞的模型,在之前的多線程的三種實現中,不管是繼承thread類還是實現runnable接口,都無法保證獲取到之前的執行結果。通過實現Callback接口,並用Future可以來接收多線程的執行結果。

Future表示一個可能還沒有完成的異步任務的結果,針對這個結果可以添加Callback以便在任務執行成功或失敗後作出相應的操作。

Future

看一下Future接口,只有五個方法比較簡單

boolean cancel(boolean mayInterruptIfRunning);//取消任務,如果已經完成或者已經取消,就返回失敗

boolean isCancelled();//查看任務是否取消

boolean isDone();//查看任務是否完成

V get() throws InterruptedException, ExecutionException;//剛纔用到了,查看結果,任務未完成就一直阻塞

V get(long timeout, TimeUnit unit)//同上,但是加了一個過期時間,防止長時間阻塞,主線程也做不了事情

throws InterruptedException, ExecutionException, TimeoutException;

CompletableFuture

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定義

public class CompletableFuture<T> implements Future<T>, CompletionStage<T>

CompletableFuture實現了兩個接口,一個是Future.一個是CompletionStage;future算是一種模式,對結果異步結果的封裝,相當於異步結果,而CompletionStage相當於完成階段,多個CompletionStage可以以流水線的方式組合起來,共同完成任務. 

CompletableFuture使用例子

首先說明一下已Async結尾的方法都是可以異步執行的,如果指定了線程池,會在指定的線程池中執行,如果沒有指定,默認會在ForkJoinPool.commonPool()中執行

先定義一個獲取線程名字的函數用來查詢執行當前任務的現場

    public static String getThreadName() {
        return Thread.currentThread().getName() + "=>";
    }

1,先看一個最簡單的例子

    public static void testNew() throws Exception {
        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() + result);
    }

執行結果

2,運行一個簡單的沒有返回值的異步任務 

    public static void testNewVoid() throws Exception {
        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() + " 結束。");
    }

 從上面我們可以看到CompletableFuture默認運行使用的是ForkJoin的的線程池。當然,你也可以用lambda表達式使得代碼更精簡。

3,運行一個有返回值的異步任務

    public static void testAsync() throws Exception {
        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我們也可以換成Executor相關的Pool,其api都有支持如下

高級的使用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());
    }

 (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");
    }

(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池的線程,但其實上面的每一步都是支持異步運行的,其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());
    }

我們可以看到,ForkJoin池的線程1,執行了前面的三個任務,但是第二個任務的子任務,因爲我們了使用也異步提交所以它用的線程是ForkJoin池的線程2,最終由於main線程處執行了get是最後結束的。

還有一點需要注意:

ForkJoinPool所有的工作線程都是守護模式的,也就是說如果主線程退出,那麼整個處理任務都會結束,而不管你當前的任務是否執行完。如果需要主線程等待結束,可採用ExecutorsThreadPool,如下:

ExecutorService pool = Executors.newFixedThreadPool(5);
final CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
                ... }, pool);

(4)thenCompose合併兩個有依賴關係的CompletableFutures的執行結果

thenCompose 方法允許你對兩個 CompletionStage 進行流水線操作,第一個操作完成時,將其結果作爲參數傳遞給第二個操作

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、thenAcceptBoth,合併兩個沒有依賴關係的CompletableFutures任務

thenCombine、thenAcceptBoth 都是用來合併任務 —— 等待兩個 CompletionStage 的任務都執行完成後,把兩個任務的結果一併來處理。區別在於 thenCombine 有返回值;thenAcceptBoth 無返回值。

thenCombine :

    public static void asyncCombine() throws ExecutionException, InterruptedException {
        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());
    }

thenAcceptBoth :

    public static void thenAcceptBoth() throws Exception {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "hello";
        });
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "world";
        });
        CompletableFuture<Void> both = future1.thenAcceptBoth(future2, (s1, s2) -> System.out.println(s1 + " " + s2));
        both.get();
    }

 (6)合併多個任務的結果allOf與anyOf

上面說的是兩個任務的合併,那麼多個任務需要使用allOf或者anyOf方法。

allOf適用於,你有一系列獨立的future任務,你想等其所有的任務執行完後做一些事情。舉個例子,比如我想下載100個網頁,傳統的串行,性能肯定不行,這裏我們採用異步模式,同時對100個網頁進行下載,當所有的任務下載完成之後,我們想判斷每個網頁是否包含某個關鍵詞。

下面我們通過隨機數來模擬上面的這個場景如下:

    public static void mutilTaskTest() throws ExecutionException, InterruptedException {
        //添加n個任務
        CompletableFuture<Double> array[] = new CompletableFuture[5];
        for (int i = 0; i < 5; 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);
    }

注意其中的join方法和get方法類似,僅僅在於在Future不能正常完成的時候拋出一個unchecked的exception,這可以確保它用在Stream的map方法中,直接使用get是沒法在map裏面運行的。

anyOf方法,也比較簡單,意思就是隻要在多個future裏面有一個返回,整個任務就可以結束,而不需要等到每一個future結束。

    public static void anyOf() throws ExecutionException, InterruptedException {
        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());
    }

 注意由於Anyof返回的是其中任意一個Future所以這裏沒有明確的返回類型,統一使用Object接受,留給使用端處理。

(7)exceptionally異常處理

異常處理是異步計算的一個重要環節,下面看看如何在CompletableFuture中使用:

    public static void testEX() throws ExecutionException, InterruptedException {
        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());
    }

 此外還有另外一種異常捕捉方法handle,無論發生異常都會執行,示例如下:

    public static void testEX() throws ExecutionException, InterruptedException {
        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方法。

總結

1. runAsync、supplyAsync

// 無返回值
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
// 有返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

2. whenComplete、whenCompleteAsync

// 執行完成時,當前任務的線程執行繼續執行 whenComplete 的任務。
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
// 執行完成時,把 whenCompleteAsync 這個任務提交給線程池來進行執行。
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)

3. thenApply、handle

//當一個線程依賴另一個線程時,可以使用 thenApply 方法來把這兩個線程串行化<br>public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)<br>//與thenApply的區別是可能是新的線程
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
//與thenApply效果差不多,出現異常不會走thenApply,handle就可以
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);

4. thenAccept、thenRun

//thenAccept 接收任務的處理結果,並消費處理。無返回結果。
public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);
//thenRun 跟 thenAccept 方法不一樣的是,不關心任務的處理結果。只要上面的任務執行完成,就開始執行 thenRun。
public CompletionStage<Void> thenRun(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action,Executor executor);

5. thenCombine、thenAcceptBoth

public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);
 
public <U,V> CompletionStage<V> thenAcceptBoth(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenAcceptBothAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenAcceptBothAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);

thenCombine、thenAcceptBoth 都是用來合併任務 —— 等待兩個 CompletionStage 的任務都執行完成後,把兩個任務的結果一併來處理。區別在於 thenCombine 有返回值;thenAcceptBoth 無返回值。

6. applyToEither、acceptEither、runAfterEither、runAfterBoth

  • applyToEither:兩個 CompletionStage,誰執行返回的結果快,就用那個 CompletionStage 的結果進行下一步的處理,有返回值。
  • acceptEither:兩個 CompletionStage,誰執行返回的結果快,就用那個 CompletionStage 的結果進行下一步的處理,無返回值。
  • runAfterEither:兩個 CompletionStage,任何一個完成了,都會執行下一步的操作(Runnable),無返回值。
  • runAfterBoth:兩個 CompletionStage,都完成了計算纔會執行下一步的操作(Runnable),無返回值。

7. thenCompose

thenCompose 方法允許你對兩個 CompletionStage 進行流水線操作,第一個操作完成時,將其結果作爲參數傳遞給第二個操作

JDK9 CompletableFuture 類增強的主要內容

(1)支持對異步方法的超時調用

orTimeout()
completeOnTimeout()

(2)支持延遲調用

Executor delayedExecutor(long delay, TimeUnit unit, Executor executor)
Executor delayedExecutor(long delay, TimeUnit unit)

轉載

https://www.cnblogs.com/liangsonghua/p/www_liangsonghua_me_37.html

https://cloud.tencent.com/developer/article/1366581

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章