CompletableFuture實現異步編排

爲什麼需要異步執行?

場景:電商系統中獲取一個完整的商品信息可能分爲以下幾步:①獲取商品基本信息 ②獲取商品圖片信息 ③獲取商品促銷活動信息 ④獲取商品各種類的基本信息 等操作,如果使用串行方式去執行這些操作,假設每個操作執行1s,那麼用戶看到完整的商品詳情就需要4s的時間,如果使用並行方式執行這些操作,可能只需要1s就可以完成。所以這就是異步執行的好處。

JDK5的Future接口

Future接口用於代表異步計算的結果,通過Future接口提供的方法可以查看異步計算是否執行完成,或者等待執行結果並獲取執行結果,同時還可以取消執行。

列舉Future接口的方法:

  • get()獲取任務執行結果,如果任務還沒完成則會阻塞等待直到任務執行完成。如果任務被取消則會拋出CancellationException異常,如果任務執行過程發生異常則會拋出ExecutionException異常,如果阻塞等待過程中被中斷則會拋出InterruptedException異常。
  • get(long timeout,Timeunit unit)帶超時時間的get()方法,如果阻塞等待過程中超時則會拋出TimeoutException異常。
  • cancel()用於取消異步任務的執行。如果異步任務已經完成或者已經被取消,或者由於某些原因不能取消,則會返回false。如果任務還沒有被執行,則會返回true並且異步任務不會被執行。如果任務已經開始執行了但是還沒有執行完成,若mayInterruptIfRunning爲true,則會立即中斷執行任務的線程並返回true,若mayInterruptIfRunning爲false,則會返回true且不會中斷任務執行線程。
  • isCanceled()判斷任務是否被取消,如果任務在結束(正常執行結束或者執行異常結束)前被取消則返回true,否則返回false。
  • isDone()判斷任務是否已經完成,如果完成則返回true,否則返回false。需要注意的是:任務執行過程中發生異常、任務被取消也屬於任務已完成,也會返回true。

使用Future接口和Callable接口實現異步執行:

public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    // 獲取商品基本信息(可以使用Lambda表達式簡化Callable接口,這裏爲了便於觀察不使用)
    Future<String> future1 = executorService.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "獲取到商品基本信息";
        }
    });
    // 獲取商品圖片信息
    Future<String> future2 = executorService.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "獲取商品圖片信息";
        }
    });
    // 獲取商品促銷信息
    Future<String> future3 = executorService.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "獲取商品促銷信息";
        }
    });
    // 獲取商品各種類基本信息
    Future<String> future4 = executorService.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "獲取商品各種類基本信息";
        }
    });
        // 獲取結果
    try {
        System.out.println(future1.get());
        System.out.println(future2.get());
        System.out.println(future3.get());
        System.out.println(future4.get());
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }finally {
        executorService.shutdown();
    }
}

既然Future可以實現異步執行並獲取結果,爲什麼還會需要CompletableFuture?

簡述一下Future接口的弊端:

  • 不支持手動完成
    • 當提交了一個任務,但是執行太慢了,通過其他路徑已經獲取到了任務結果,現在沒法把這個任務結果通知到正在執行的線程,所以必須主動取消或者一直等待它執行完成。
  • 不支持進一步的非阻塞調用
    • 通過Future的get()方法會一直阻塞到任務完成,但是想在獲取任務之後執行額外的任務,因爲 Future 不支持回調函數,所以無法實現這個功能。
  • 不支持鏈式調用
    • 對於Future的執行結果,想繼續傳到下一個Future處理使用,從而形成一個鏈式的pipline調用,這在 Future中無法實現。
  • 不支持多個 Future 合併
    • 比如有10個Future並行執行,想在所有的Future運行完畢之後,執行某些函數,是無法通過Future實現的。
  • 不支持異常處理
    • Future的API沒有任何的異常處理的api,所以在異步運行時,如果出了異常問題不好定位。

使用Future接口可以通過get()阻塞式獲取結果或者通過輪詢+isDone()非阻塞式獲取結果,但是前一種方法會阻塞,後一種會耗費CPU資源,所以JDK的Future接口實現異步執行對獲取結果不太友好,所以在JDK8時推出了CompletableFuture實現異步編排


CompletableFuture的使用

CompletableFuture概述

JDK8中新增加了一個包含50個方法左右的類CompletableFuture,提供了非常強大的Future的擴展功能,可以幫助我們簡化異步編程的複雜性,提供了函數式編程的能力,可以通過回調的方式處理計算結果,並且提供了轉換和組合CompletableFuture的方法

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

CompletableFuture類實現了Future接口和CompletionStage接口,即除了可以使用Future接口的所有方法之外,CompletionStage<T>接口提供了更多方法來更好的實現異步編排,並且大量的使用了JDK8引入的函數式編程概念。後面會細緻的介紹常用的API。


① 創建CompletableFuture的方式

  1. 使用new關鍵字創建
// 無返回結果
CompletableFuture<String> completableFuture = new CompletableFuture<>();
// 已知返回結果
CompletableFuture<String> completableFuture = new CompletableFuture<>("result");
// 已知返回結果(底層其實也是帶參數的構造器賦值)
CompletableFuture<String> completedFuture = CompletableFuture.completedFuture("result");

創建一個返回結果類型爲String的CompletableFuture,可以使用Future接口的get()方法獲取該值(同樣也會阻塞)。

可以使用無參構造器返回一個沒有結果的CompletableFuture,也可以通過構造器的傳參CompletableFuture設置好返回結果,或者使用CompletableFuture.completedFuture(U value)構造一個已知結果的CompletableFuture。

  1. 使用CompletableFuture類的靜態工廠方法(常用)
  1. runAsync() 無返回值
// 使用默認線程池
public static CompletableFuture<Void> runAsync(Runnable runnable)
// 使用自定義線程池(推薦)
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor) 

runAsync()方法的參數是Runnable接口,這是一個函數式接口,不允許返回值。當需要異步操作且不關心返回結果的時候可以使用runAsync()方法。

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        // 通過Lambda表達式實現Runnable接口
        CompletableFuture.runAsync(()-> System.out.println("獲取商品基本信息成功"), executor).get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}
  1. supplyAsync() 有返回值
// 使用默認線程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
// 使用自定義線程池(推薦)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)

supplyAsync()方法的參數是Supplier<U>供給型接口(無參有返回值),這也是一個函數式接口,U是返回結果值的類型當需要異步操作且關心返回結果的時候,可以使用supplyAsync()方法。

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        // 通過Lambda表達式實現執行內容,並返回結果通過CompletableFuture接收
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("獲取商品信息成功");
            return "信息";
        }, executor);
        // 輸出結果
        System.out.println(completableFuture.get());
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}  

關於第二個參數Executor executor說明

在沒有指定第二個參數(即沒有指定線程池)時,CompletableFuture直接使用默認的ForkJoinPool.commonPool()作爲它的線程池執行異步代碼。


② 獲得異步執行結果

  1. get() 阻塞式獲取執行結果
public T get() throws InterruptedException, ExecutionException

該方法調用後如果任務還沒完成則會阻塞等待直到任務執行完成。如果任務執行過程發生異常則會拋出ExecutionException異常,如果阻塞等待過程中被中斷則會拋出InterruptedException異常。

  1. get(long timeout, TimeUnit unit) 帶超時的阻塞式獲取執行結果
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException

該方法調用後如果如果任務還沒完成則會阻塞等待直到任務執行完成或者超出timeout時間,如果阻塞等待過程中超時則會拋出TimeoutException異常。

  1. getNow(T valueIfAbsent) 立刻獲取執行結果
public T getNow(T valueIfAbsent)

該方法調用後,會立刻獲取結果不會阻塞等待。如果任務完成則直接返回執行完成後的結果,如果任務沒有完成,則返回調用方法時傳入的參數valueIfAbsent值。

  1. join() 不拋異常的阻塞時獲取執行結果
public T join()

該方法和get()方法作用一樣,只是不會拋出異常

  1. complete(T value) 主動觸發計算,返回異步是否執行完畢
public boolean complete(T value)

該方法調用後,會主動觸發計算結果,如果此時異步執行並沒有完成(此時boolean值返回true),則通過get()拿到的數據會是complete()設置的參數value值,如果此時異步執行已經完成(此時boolean值返回false),則通過get()拿到的就是執行完成的結果。

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        // 通過Lambda表達式實現執行內容,並返回結果通過CompletableFuture接收
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            // 休眠2秒,使得異步執行變慢,會導致主動觸發計算先執行,此時返回的get就是555
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            return 666;
        }, executor);
        // 主動觸發計算,判斷異步執行是否完成
        System.out.println(completableFuture.complete(555));
        // 輸出結果
        System.out.println(completableFuture.get());
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
    true
    555
**/

③ 對執行結果進行處理

whenComplete 等待前面任務執行完再執行當前處理

public CompletableFuture<T> whenComplete(
        BiConsumer<? super T, ? super Throwable> action)

在創建好的初始任務或者是上一個任務後通過鏈式調用該方法,會在之前任務執行完成後繼續執行whenComplete裏的內容(whenComplete傳入的action只是對之前任務的結果進行處理),即使用該方法可以避免前面說到的Future接口的問題,不再需要通過阻塞或者輪詢的方式去獲取結果,而是通過調用該方法等任務執行完畢自動調用。

該方法的參數爲BiConsumer<? super T, ? super Throwable> action消費者接口,可以接收兩個參數,一個是任務執行完的結果,一個是執行任務時的異常

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture.supplyAsync(() -> 666, executor)
                .whenComplete((res, ex) -> System.out.println("任務執行完畢,結果爲" + res + " 異常爲" + ex)
                );
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
    任務執行完畢,結果爲666 異常爲null
**/

除了上述的方法外,還有一些類似的方法如XXXAsync()或者是XXXAsync(XX,Executor executor),對於這些方法,這裏統一說明,後續文章中將不會再列舉

public CompletableFuture<T> whenCompleteAsync(
        BiConsumer<? super T, ? super Throwable> action)

public CompletableFuture<T> whenCompleteAsync(
        BiConsumer<? super T, ? super Throwable> action, Executor executor)

XXXAsync():表示上一個任務執行完成後,不會再使用之前任務中的線程,而是重新使用從默認線程(ForkJoinPool 線程池)中重新獲取新的線程執行當前任務

XXXAsync(XX,Executor executor):表示不會沿用之前任務的線程,而是使用自己第二個參數指定的線程池重新獲取線程執行當前任務


④ 對執行結果進行消費

  1. thenRun 前面任務執行完後執行當前任務,不關心前面任務的結果,也沒返回值
public CompletableFuture<Void> thenRun(Runnable action)

CompletableFuture.supplyAsync(actionA).thenRun(actionB)像這樣鏈式調用該方法表示:執行任務A完成後接着執行任務B,但是任務B不需要A的結果,並且執行完任務B也不會返回結果

thenRun(Runnable action)的參數爲Runnable接口即沒有傳入參數

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture.supplyAsync(() -> 666, executor)
                    .thenRun(() -> System.out.println("我都沒有參數怎麼拿到之前的結果,我也沒有返回值。")
                );
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
    我都沒有參數怎麼拿到之前的結果,我也沒有返回值。
**/
  1. thenAccept 前面任務執行完後執行當前任務,消費前面的結果,沒有返回值
public CompletableFuture<Void> thenAccept(Consumer<? super T> action)

CompletableFuture.supplyAsync(actionA).thenRun(actionB)像這樣鏈式調用該方法表示:執行任務A完成後接着執行任務B,而且任務B需要A的結果,但是執行完任務B不會返回結果

thenAccept(Consumer<? super T> action)的參數爲消費者接口,即可以傳入一個參數,該參數爲上一個任務的執行結果。

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture.supplyAsync(() -> 666, executor)
                .thenAccept((res) -> System.out.println("我能拿到上一個的結果" + res + ",但是我沒法傳出去。")
                );
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
    我能拿到上一個的結果666,但是我沒法傳出去。
**/
  1. thenApply 前面任務執行完後執行當前任務,消費前面的結果,具有返回值
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)

CompletableFuture.supplyAsync(actionA).thenRun(actionB)像這樣鏈式調用該方法表示:執行任務A完成後接着執行任務B,而且任務B需要A的結果,並且執行完任務B需要有返回結果

thenApply(Function<? super T,? extends U> fn)的參數爲函數式接口,即可以傳入一個參數類型爲T,該參數是上一個任務的執行結果,並且函數式接口需要有返回值,類型爲U。

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture.supplyAsync(() -> 666, executor)
                .thenApply((res) -> {
                        System.out.println("我能拿到上一個的結果" + res + "並且我要將結果傳出去");
                        return res;
                    }
                ).whenComplete((res, ex) -> System.out.println("結果" + res));
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}
/**
輸出結果:
    我能拿到上一個的結果666並且我要將結果傳出去
    結果666
**/

⑤ 異常處理

  1. exceptionally 異常捕獲,只消費前面任務中出現的異常信息,具有返回值
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

可以通過鏈式調用該方法來獲取異常信息,並且具有返回值。如果某一個任務出現異常被exceptionally捕獲到則剩餘的任務將不會再執行。類似於Java異常處理的catch。

exceptionally(Function<Throwable, ? extends T> fn)的參數是函數式接口,具有一個參數以及返回值,該參數爲前面任務的異常信息。

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture.supplyAsync(() -> {
                    if (Math.random() < 0.5) throw new RuntimeException("error");
                    return 666;
                }, executor)
                .thenApply((res) -> {
                    System.out.println("不出現異常,結果爲" + res);
                    return res;
                }).exceptionally((ex) -> {
                    ex.printStackTrace();
                    return -1;
                });
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
// 這是不出現異常的情況
不出現異常,結果爲666

// 這是出現異常的情況
java.util.concurrent.CompletionException: java.lang.RuntimeException: error
        at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:314)
        at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:319)
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1702)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.RuntimeException: error
        at com.xqsr.review.thread.ThreadTest.lambda$main$0(ThreadTest.java:15)
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
        ... 3 more
**/
  1. handle 異常處理,消費前面的結果及異常信息,具有返回值,不會中斷後續任務
public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)

可以通過鏈式調用該方法可以跟thenApply()一樣可以消費前面任務的結果並完成自己任務內容,並且具有返回值。不同之處在於出現異常也可以接着往下執行,根據異常參數做進一步處理。

handle(BiFunction<? super T, Throwable, ? extends U> fn)的參數是消費者接口,一個參數是任務執行結果,一個是異常信息,並且具有返回值。

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture.supplyAsync(() -> 666, executor)
                .thenApply((res) -> {
                    if (Math.random() < 0.5) throw new RuntimeException("error");
                    return res;
                }).handle((res, ex) -> {
                    System.out.println("結果" + res + "(null表示之前出現異常導致結果無法傳過來)");
                    return res == null ? -1 : res;
                }).thenApply((res) -> {
                    System.out.println("結果爲" + res + "(-1表示之前出現異常,經過handler使得結果處理成-1)");
                    return res;
                }).exceptionally((ex) -> {
                    ex.printStackTrace();
                    return -1;
                });
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
// 這是不出現異常的情況
結果666(null表示之前出現異常導致結果無法傳過來)
結果爲666(-1表示之前出現異常,經過handler使得結果處理成-1)

// 這是出現異常的情況
結果null(null表示之前出現異常導致結果無法傳過來)
結果爲-1(-1表示之前出現異常,經過handler使得結果處理成-1)
**/

可以看到通過handle類似於Java異常處理的finally,出現異常並不會像使用exceptionally那樣中斷後續的任務,而是繼續執行,可以通過handle爲之前出現異常無法獲得的結果重新賦值(根據業務需求設置安全值之類的)。


⑥ 兩組任務按順序執行

thenCompose 實現兩組任務按前後順序執行

public <U> CompletableFuture<U> thenCompose(
    Function<? super T, ? extends CompletionStage<U>> fn)
複製代碼

A.thenCompose(B)相當於任務A要排在任務B前面,即順序的執行任務A、任務B。該方法的參數是函數式接口,函數式接口的參數是調用者的執行結果,返回值是另一個任務B。

public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture<Integer> actionA = CompletableFuture.supplyAsync(() -> {
            System.out.println("任務A先執行結果爲666");
            return 666;
        }, executor);

        actionA.thenCompose((res) ->  CompletableFuture.supplyAsync(() -> {
            System.out.println("任務B後執行結果加上333");
            return 333 + res;
        })).whenComplete((res, ex) -> System.out.println(res));
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
    任務A先執行結果爲666
    任務B後執行結果加上333
    999
**/

⑦ 兩組任務誰快用誰

applyToEither 比較兩組任務執行速度,誰快消費誰的執行結果

public <U> CompletableFuture<U> applyToEither(
        CompletionStage<? extends T> other, Function<? super T, U> fn)

該方法用於比較兩組任務的執行速度,誰先執行完就用誰的執行結果

傳入參數說明:第一個參數傳入的是另一個任務的執行內容,第二個參數傳入的是最終這兩個任務誰快返回誰的結果,並通過當前函數式接口進行接收和處理(使用函數式接口,有參且有返回值)。

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture<Integer> actionA = CompletableFuture.supplyAsync(() -> {
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("任務A等待久一點,執行結果爲555");
            return 555;
        }, executor);

        actionA.applyToEither(CompletableFuture.supplyAsync(() -> {
            System.out.println("任務B很快,執行結果爲666");
            return 666;
        }), (res) -> {
            System.out.println("最終使用的執行結果爲" + res);
            return res;
        });
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}
/**
輸出結果:
    任務B很快,執行結果爲666
    最終使用的執行結果爲666
    任務A等待久一點,執行結果爲555
**/

除了applyToEither對任務最終結果進行獲取並消費,並且具有返回值的方法外,還有兩個類似的方法。

// 這個方法效果和上面的一樣,比誰快拿誰的結果,不同的是這個方法只消費不具有返回值
public CompletableFuture<Void> acceptEither(
        CompletionStage<? extends T> other, Consumer<? super T> action)
// 這個方法效果和上面的一樣,比誰快拿誰的結果,不同的是這個方法不消費結果也不具有返回值
public CompletableFuture<Void> runAfterEither(
        CompletionStage<?> other, Runnable action)

⑧ 兩組任務完成後合併

thenCombine 等待兩組任務執行完畢後,合併兩組任務的執行結果

 public <U,V> CompletableFuture<V> thenCombine(
        CompletionStage<? extends U> other,
        BiFunction<? super T,? super U,? extends V> fn)

該方法用於兩組任務都完成後,將兩組任務的執行結果一起交給當前方法的BiFunction處理。先完成的任務會等待後者任務完成。

傳入參數說明:第一個參數傳入的是另一個任務的執行內容,第二個參數傳入的是帶兩個參數的函數式接口(第一個參數是任務1的執行結果,第二個參數是任務2的執行結果,具有返回值)。

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture<Integer> actionA = CompletableFuture.supplyAsync(() -> {
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("任務A等待久一點,執行結果爲333");
            return 333;
        }, executor);

        CompletableFuture<Integer> actionB = CompletableFuture.supplyAsync(() -> {
            System.out.println("任務B很快,執行結果爲666");
            return 666;
        }, executor);

        actionA.thenCombine(actionB, (res1, res2) -> {
            System.out.println("最終使用的執行結果爲" + (res1 + res2));
            return res1 + res2;
        });
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
    任務B很快,執行結果爲666
    任務A等待久一點,執行結果爲333
    最終使用的執行結果爲999
**/

除了thenCombine對任務最終結果進行獲取並消費,並且具有返回值的方法外,還有兩個類似的方法。

// 這個方法效果和上面的一樣,獲取合併結果,不同的是這個方法只消費不具有返回值
public <U> CompletableFuture<Void> thenAcceptBoth(
    CompletionStage<? extends U> other,
    BiConsumer<? super T, ? super U> action)
// 這個方法效果和上面的一樣,獲取合併結果,不同的是這個方法不消費結果也不具有返回值
public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,
                                                Runnable action)

⑨ 多任務組合

  1. allOf 實現並行地執行多個任務,等待所有任務執行完成(無需考慮執行順序)
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

該方法可以實現並行地執行多個任務,適用於多個任務沒有依賴關係,可以互相獨立執行的,傳入參數爲多個任務,沒有返回值。

allOf()方法會等待所有的任務執行完畢再返回,可以通過get()阻塞確保所有任務執行完畢

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture<Void> actionA = CompletableFuture.runAsync(() -> {
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("任務A等待2秒後執行完畢");
        }, executor);

        CompletableFuture<Void> actionB = CompletableFuture.runAsync(() -> {
            System.out.println("任務B很快執行完畢");
        }, executor);

        CompletableFuture<Void> actionC = CompletableFuture.runAsync(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("任務C等待1秒後執行完畢");
        }, executor);

        CompletableFuture<Void> actionD = CompletableFuture.runAsync(() -> {
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("任務D等待5秒後執行完畢");
        }, executor);

        CompletableFuture.allOf(actionA, actionB, actionC, actionD).get();
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
    任務B很快執行完畢
    任務C等待1秒後執行完畢
    任務A等待2秒後執行完畢
    任務D等待5秒後執行完畢
**/
  1. anyOf 實現並行地執行多個任務,只要有個一個完成的便會返回執行結果
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

該方法可以實現並行地執行多個任務,傳入參數爲多個任務,具有返回值。該方法不會等待所有任務執行完成後再返回結果,而是當有一個任務完成時,便會返回那個任務的執行結果

// 例子
public static void main(String[] args) {
    // 快速創建線程池
    ExecutorService executor = Executors.newFixedThreadPool(4);
    try {
        CompletableFuture<Integer> actionA = CompletableFuture.supplyAsync(() -> {
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("任務A等待2秒後執行完畢");
            return 555;
        }, executor);

        CompletableFuture<Integer> actionB = CompletableFuture.supplyAsync(() -> {
            System.out.println("任務B很快執行完畢");
            return 666;
        }, executor);

        CompletableFuture<Integer> actionC = CompletableFuture.supplyAsync(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("任務C等待1秒後執行完畢");
            return 999;
        }, executor);

        CompletableFuture<Integer> actionD = CompletableFuture.supplyAsync(() -> {
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("任務D等待5秒後執行完畢");
            return 888;
        }, executor);

        System.out.println("最先執行完的返回結果爲" + CompletableFuture.anyOf(actionA, actionB, actionC, actionD).get());
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        executor.shutdown();
    }
}

/**
輸出結果:
    任務B很快執行完畢
    最先執行完的返回結果爲666
    任務C等待1秒後執行完畢
    任務A等待2秒後執行完畢
    任務D等待5秒後執行完畢
**/

一個使用CompletableFuture異步編排的例子

不需要關心例子中的業務內容,使用時按照自己業務的需求,對不同的需求調用不同API即可。

編寫任務時主要關心以下幾點: ① 是否需要消費之前任務的結果 ② 是否需要返回結果給其他任務消費 ③ 是否要求順序執行(是否允許並行,有沒有前置要求)

/**
 * 該方法用於獲取單個商品的所有信息
 * 1\. 商品的基本信息
 * 2\. 商品的圖片信息
 * 3\. 商品的銷售屬性組合
 * 4\. 商品的各種分類基本信息
 * 5\. 商品的促銷信息
 */
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
    // 創建商品Vo通過各個任務去完善Vo的信息
    SkuItemVo skuItemVo = new SkuItemVo();

    // 獲取商品基本信息 查詢到後設置進Vo中,返回基本信息給後續任務消費 (使用自定義的線程池進行異步)
    CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
        SkuInfoEntity info = this.getById(skuId);
        skuItemVo.setInfo(info);
        return info;
    }, executor);

    // 獲取商品的圖片信息 獲取後設置進Vo中,此處不需要消費圖片信息,也不需要返回結果。所以使用runAsync即可
    CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
        List<SkuImagesEntity> imagesEntities = skuImagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(imagesEntities);
    }, executor);

    // 獲取商品銷售屬性 因爲要利用之前查詢到的基本信息,但後續任務不需要消費銷售屬性(不需要返回結果),所以使用thenAcceptAsync消費之前的基本信息,不返回銷售信息。
    CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
        List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrBySpuId(res.getSpuId());
        skuItemVo.setSaleAttr(saleAttrVos);
    }, executor);

    // 獲取商品各分類基本信息,同樣要消費之前的基本信息,但無需返回,所以使用thenAcceptAsync即可
    CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
        SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
        skuItemVo.setDesc(spuInfoDescEntity);
    }, executor);

    // 獲取商品的促銷信息 這個也不需要消費之前任務的結果,也不需要返回結果。所以直接使用runAsync即可
    CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
        R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);
        if (skuSeckilInfo.getCode() == 0) {
            SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {
            });
            skuItemVo.setSeckillSkuVo(seckilInfoData);

            if (seckilInfoData != null) {
                long currentTime = System.currentTimeMillis();
                if (currentTime > seckilInfoData.getEndTime()) {
                    skuItemVo.setSeckillSkuVo(null);
                }
            }
        }
    }, executor);

    // 使用allOf()組合所有任務,並且使用get()阻塞,等待所有任務完成。
        // 補充:infoFuture不能放入allOf中,因爲allOf是並行無序執行(需要多個條件是無依賴性的)的,當上面任務中有需要消費infoFuture的結果,所以需要先執行infoFuture。
    CompletableFuture.allOf(imageFuture,saleAttrFuture,descFuture,seckillFuture).get();

    // 最後返回商品Vo
    return skuItemVo;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章