JAVA 多線程與高并發學習筆記(十八)——CompletableFuture

本部分介紹Java 8 中提供的具備異步回調能力的工具類——CompletableFuture,該類實現了Future接口,還具備函數式編程能力。

CompletableFuture詳解

CompletableFuture類實現了Future和CompletionStage兩個接口,該類的實例作為一個異步任務。

CompletionStage接口

CompletionStage代表某個同步或者異步計算的一個階段,或者一些列異步任務中的一個子任務。

每個CompletionStage子任務所包裝的可以是一個Function、Consumer或者Runnable函數式接口實例。這三個函數式接口特點如下:

  1. Function接口的特點是有輸入、有輸出。
  2. Runnable接口的特點是無輸入、無輸出。
  3. Consumer接口的特點是有輸入、無輸出。

多個CompletionStage構成了一條任務流水線,多個子任務之間可以使用鏈式調用,下面是個簡單的例子:

oneStage.thenApply(x -> square(x))
            .thenAccept(y -> System.out.println(y))
            .thenRun(() -> System.out.println())

上例子說明如下:

  1. oneStage是一個CompletionStage子任務。
  2. x -> square(x) 是一個Function類型的Lambda表達式,被thenApply方法包裝成了CompletionStage子任務,它又包含輸入和輸出。
  3. y -> System.out.println(y)是一個Consumer類型的Lambda表達式,被thenAccept包裝成了一個CompletionStage子任務,它只有輸入(即上個任務的輸出)。
  4. () -> System.out.println()是一個Runnable類型的Lambda表達式,被thenRun方法包裝成了一個CompletionStage子任務,它沒有輸入輸出。

使用runAsync和supplyAsync創建子任務

CompletableFuture定義了一組用于創建CompletionStage子任務的方法。

// 子任務包裝一個Runnable實例,并調用ForkJoinPool。commonPool線程池來執行
public static CompletableFuture<Void> runAsync(Runnable runnable)

// 子任務包裝一個Runnable實例,并調用指定的executor線程池來執行
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)

// 子任務包裝一個Supplier實例,并調用ForkJoinPool。commonPool線程池來執行
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

// 子任務包裝一個Supplier實例,并調用指定的executor線程池來執行
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

執行子任務時,如果沒有執行Executor線程池,默認情況下會使用公共的ForkJoinPool線程池。

設置子任務回調鉤子

可以為CompletionStage子任務設置特定的回調鉤子,當計算結果完成或者拋出異常的時候,執行這些特定的鉤子。

設置子任務回調鉤子的主要函數如下:

// 設置子任務完成時的回調鉤子
public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throeable> action)

// 設置子任務完成時的回調鉤子,可能不在同一線程執行
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)

// 設置子任務完成時的回調鉤子,提交給線程池executor執行
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor)

// 設置異常處理的回調鉤子
public CompletableFuture<T> exceptionally(Fuction<Throwable, ? extends T> fn)

下面看個簡單例子:


public class CompletableFutureDemo {

    public static void main(String[] args) throws Exception {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("拋出異常");
            throw new RuntimeException("發生異常");
        });

        // 設置異步任務執行完成后的回調鉤子
        future.whenComplete(new BiConsumer<Void, Throwable>() {
            @Override
            public void accept(Void aVoid, Throwable throwable) {
                System.out.println("執行完成");
            }
        });

        future.exceptionally(new Function<Throwable, Void>() {
            @Override
            public Void apply(Throwable t) {
                System.out.println("執行失敗:" + t.getMessage());
                return null;
            }
        });

        future.get();
    }
}

運行結果:

拋出異常
執行完成
執行失敗:java.lang.RuntimeException: 發生異常

調用cancel方法取消CompletableFuture時,任務被視為異常完成,completeExceptionally方法所設置的異常回調鉤子也會被執行。

如果沒有設置異常回調鉤子,發生內部異常時會有兩種情況發生:

  1. 在調用get方法啟動任務時,如果遇到內部異常,get方法就會拋出ExecutionException。
  2. 在調用join和getNow啟動任務時(大多數情況下都是如此),如果遇到內部異常,會拋出CompletionException。

調用handle方法統一處理異常和結果

除了分別通過whenComplete、exceptionally設置完成鉤子、異常鉤子之外,還可以調用handle方法統一處理結果和異常。

handle方法有3個重載版本:

// 在執行任務的同一個線程中處理異常和結果
public<U> CompletionStage<U> handle (BiFunction<? super T, Throwable, ? extends U> fn);

// 可能不再執行任務的同一個線程中處理異常和結果
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);

// 在指定線程池executor中處理異常和結果
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor);

將前面的例子改成handle版本。


public class CompletableFutureDemo {

    public static void main(String[] args) throws Exception {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("拋出異常");
            throw new RuntimeException("發生異常");
        });

        future.handle(new BiFunction<Void, Throwable, Void>() {
            @Override
            public Void apply(Void input, Throwable throwable) {
                if(throwable == null) {
                    System.out.println("沒有發生異常");
                } else {
                    System.out.println("發生了異常");
                }
                return null;
            }
        });

        future.get();
    }
}

線程池的使用

默認情況下,通過靜態方法runAsync和supplyAsync創建的CompletableFuture任務會使用公共的ForkJoinPool線程池。默認的線程數是CPU的核數。

如果所有CompletableFuture任務共享一個線程池,那么一旦有任務執行一些很慢的IO操作,會導致所有線程阻塞,造成線程饑餓。所以建議大家根據不同的業務類型創建不同的線程池。

異步任務的串行執行

如果兩個異步任務需要串行,可以通過CompletionStage接口的thenApply、thenAccept、thenRun和thenCompose方法實現。

theApply方法

theApply方法有三個重載版本,聲明如下:

// 后一個任務與前一個任務在同一個線程中執行
public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn)

// 后一個任務與前一個任務不在同一個線程中執行
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn)

// 后一個任務在指定的executor線程池中執行
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn, Executor executor)

參數fn表示要串行執行的第二個異步任務,泛型參數T是上一個任務所返回結果的類型,泛型參數U是當前任務的返回值類型。

看個簡單例子。

public class ThenApplyDemo {

    public static void main(String[] args) throws Exception {
        CompletableFuture<Long> future = CompletableFuture.supplyAsync(new Supplier<Long>() {
            @Override
            public Long get() {
                long firstStep = 10L + 10L;
                System.out.println("first step outcome is " + firstStep);

                return firstStep;
            }
        }).thenApplyAsync(new Function<Long, Long>() {
            @Override
            public Long apply(Long aLong) { // 參數是第一步的結果
                long secondStep = aLong * 2;
                System.out.println("Second outcome is " + secondStep);
                return secondStep;
            }
        });

        long result = future.get();
        System.out.println("outcome is " + result);
    }
}

thenRun方法

thenRun方法不關心任務的處理結果,只需要前一個任務執行完成,就開始執行后一個串行任務。

thenApply方法也有三個重載版本,聲明如下:

// 后一個任務與前一個任務在同一個線程中執行
public CompletionStage<Void> thenRun(Runnable action);

// 后一個任務與前一個任務不再同一個線程中執行
public CompletionStage<Void> thenRunAsync(Runnable action);

// 后一個任務在executor線程池中執行
public CompletionStage<Void> thenRunAsync(Runnable action, Executor executor);

thenAccept方法

thenAccept方法接收前一個任務的處理結果,但是沒有輸出。

thenAccept方法有三個重載版本,聲明如下:

// 后一個任務與前一個任務在同一個線程中執行
public CompletionStage<Void> thenAccept(Consumer<? super T> action);

// 后一個任務與前一個任務不再同一個線程中執行
public CompletionStage<Void> thenRunAsync(Consumer<? super T> action);

// 后一個任務在executor線程池中執行
public CompletionStage<Void> thenRunAsync(Consumer<? super T> action, Executor executor);

thenCompose方法

thenCompose方法在第一個任務操作完成時,將它的結果作為參數傳遞給第二個任務。

thenCompose方法有3個重載版本,聲明如下:

public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U> > fn);

public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U> > fn);

public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U> > fn, Executor executor);

thenCompose方法要求第二個任務的返回值是一個CompletionStage異步實例。

將前面的例子改成theCompose版本:


public class ThenComposeDemo {

    public static void main(String[] args) throws Exception {
        CompletableFuture<Long> future = CompletableFuture.supplyAsync(new Supplier<Long>() {
            @Override
            public Long get() {
                long firstStep = 10L + 10L;
                System.out.println("first step outcome is " + firstStep);

                return firstStep;
            }
        }).thenCompose(new Function<Long, CompletionStage<Long>>() {
            @Override
            public CompletionStage<Long> apply(Long firstStepOutcome) {
                return CompletableFuture.supplyAsync(new Supplier<Long>() {
                    @Override
                    public Long get() {
                        long secondStep = firstStepOutcome * 2;
                        System.out.println("Second outcome is " + secondStep);
                        return secondStep;
                    }
                });
            }
        });
        long result = future.get();
        System.out.println("outcome is " + result);

    }
}

異步任務的合并執行

對兩個異步任務合并可以通過CompletionStage接口的thenCombine、runAfterBoth和thenAcceptBoth三個方法來實現。

thenCombine方法

thenCombine會在兩個任務都執行完成后,把兩個任務的結果一起交給thenCombine來處理。

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
);

下面看一個使用thenCombine分三步計算(10+10)*(10+10)的例子:


public class ThenCombineDemo {

    public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future1 =
                CompletableFuture.supplyAsync(new Supplier<Integer>() {
                    @Override
                    public Integer get() {
                        Integer firstStep = 10 + 10;
                        System.out.println("firstStep outcome is " + firstStep);
                        return firstStep;
                    }
                });
        CompletableFuture<Integer> future2 =
                CompletableFuture.supplyAsync(new Supplier<Integer>() {
                    @Override
                    public Integer get() {
                        Integer secondStep = 10 + 10;
                        System.out.println("secondStep outcome is " + secondStep);
                        return secondStep;
                    }
                });
        CompletableFuture<Integer> future3 = future1.thenCombine(future2,
                new BiFunction<Integer, Integer, Integer>() {
                    @Override
                    public Integer apply(Integer integer, Integer integer2) {
                        return integer * integer2;
                    }
                });
        Integer result = future3.get();
        System.out.println(" outcome is " + result);
    }
}

runAfterBoth方法

runAfterBoth方法不關心沒異步任務的輸入參數和處理結果。

public CompletionStage<Void> runAfterBoth(
    CompletionStage<?> other, Runnable action
);

public CompletionStage<Void> runAfterBothAsync(
    CompletionStage<?> other, Runnable action
);

public CompletionStage<Void> runAfterBothAsync(
    CompletionStage<?> other, Runnable action, Executor executor
);

thenAcceptBoth方法

thenAcceptBoth方法可以接收前兩個任務的處理結果,但是第三個任務卻不返回結果。

public <U> CompletionStage<Void> thenAcceptBoth(
    CompletionStage<? extends U> other,
    BiConusmer<? super T, ? super U> action
);

public <U> CompletionStage<Void> thenAcceptBothAsync(
    CompletionStage<? extends U> other,
    BiConusmer<? super T, ? super U> action
);

public <U> CompletionStage<Void> thenAcceptBothAsync(
    CompletionStage<? extends U> other,
    BiConusmer<? super T, ? super U> action,
    Executor executor
);

allOf等待所有的任務結束

allOf會等待所有任務結束,以合并所有任務。

異步任務的選擇執行

對有兩個異步任務的選擇可以通過CompletionStage接口的applyToEither、runAfterEither和acceptEither三個方法來實現。

applyToEither方法

兩個CompletionStage誰返回結果的速度快,applyToEither方法就用這個結果進行下一步操作。

// 和other任務返回較快的結果用于執行fn回調函數
public <U> CompletionStage<U> applyToEither(
    CompletionStage<? extends T> other, Function<? super T, U> fn
);

// 功能與上一個相同,但是不一定在同一個線程執行fn
public <U> CompletionStage<U> applyToEitherAsync(
    CompletionStage<? extends T> other, Function<? super T, U> fn
);

// 功能與上一個相同,在指定線程執行fn
public <U> CompletionStage<U> applyToEitherAsync(
    CompletionStage<? extends T> other, Function<? super T, U> fn, Executor executor
);

看個例子。


public class ApplyToEitherDemo {

    public static void main(String[] args) throws Exception{
        CompletableFuture<Integer> future1 =
                CompletableFuture.supplyAsync(new Supplier<Integer>() {
                    @Override
                    public Integer get() {
                        Integer firstStep = 10 + 10;
                        System.out.println("firstStep outcome is " + firstStep);
                        return firstStep;
                    }
                });
        CompletableFuture<Integer> future2 =
                CompletableFuture.supplyAsync(new Supplier<Integer>() {
                    @Override
                    public Integer get() {
                        Integer secondStep = 100 + 100;
                        System.out.println("secondStep outcome is " + secondStep);
                        return secondStep;
                    }
                });

        CompletableFuture<Integer> future3 =
                future1.applyToEither(future2,
                        new Function<Integer, Integer>() {
                            @Override
                            public Integer apply(Integer integer) {
                                return integer;
                            }
                        });
        Integer result = future3.get();
        System.out.println("outcome is " + result);
    }
}

runAfterEither方法

runAfterEither方法的功能為前面兩個CompletionStage實例,任何一個執行完成都會執行第三部回調。

// 和other任務返回較快的結果用于執行fn回調函數
public CompletionStage<Void> runAfterEither(
    CompletionStage<?> other, Runnable fn
);

// 功能與上一個相同,但是不一定在同一個線程執行fn
public CompletionStage<Void> runAfterEitherAsync(
    CompletionStage<?> other, Runnable fn
);

// 功能與上一個相同,在指定線程執行fn
public CompletionStage<Void> runAfterEitherAsync(
    CompletionStage<?> other, Runnable fn, Executor executor
);

acceptEither方法

acceptEither用哪個最快的CompletionStage的結果作為下一步的輸入,但是第三步沒有輸出。

// 和other任務返回較快的結果用于執行fn回調函數
public CompletionStage<Void> acceptEither(
    CompletionStage<?> other, Consumer<? super T> fn
);

public CompletionStage<Void> acceptEitherAsync(
    CompletionStage<?> other, Consumer<? super T> fn
);

public CompletionStage<Void> acceptEitherAsync(
    CompletionStage<?> other, Consumer<? super T> fn, Executor executor
);

CompletableFuture的綜合案例

使用CompletableFuture實現喝茶案例。


public class CompletableFutureDemo2 {

    private static final int SLEEP_GAP = 3;

    public static void main(String[] args) {
        // 洗水壺->燒開水
        CompletableFuture<Boolean> hotJob =
                CompletableFuture.supplyAsync(() -> {
                    System.out.println("洗好水壺");
                    System.out.println("燒開水");
                    try {
                        Thread.sleep(SLEEP_GAP);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("水開了");
                    return true;
                });
        // 洗茶杯->那茶葉
        CompletableFuture<Boolean> washJob =
                CompletableFuture.supplyAsync(() -> {
                    System.out.println("洗茶杯");
                    try {
                        Thread.sleep(SLEEP_GAP);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("洗完了");

                    return true;
                });
        CompletableFuture<String> drinkJob =
                hotJob.thenCombine(washJob, (hotOK, washOk) -> {
                    if(hotOK && washOk) {
                        System.out.println("泡茶喝,茶喝完");
                        return "茶喝完了";
                    }
                    return "沒有喝到差";
                });
        System.out.println(drinkJob.join());
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372