從 5s 到 0.5s!CompletableFuture 異步任務優化技巧,確實優雅!

一個接口可能需要調用 N 個其他服務的接口,這在項目開發中還是挺常見的。舉個例子:用戶請求獲取訂單信息,可能需要調用用戶信息、商品詳情、物流信息、商品推薦等接口,最後再彙總數據統一返回。

如果是串行(按順序依次執行每個任務)執行的話,接口的響應速度會非常慢。考慮到這些接口之間有大部分都是 無前後順序關聯 的,可以 並行執行 ,就比如說調用獲取商品詳情的時候,可以同時調用獲取物流信息。通過並行執行多個任務的方式,接口的響應速度會得到大幅優化。

serial-to-parallel

對於存在前後順序關係的接口調用,可以進行編排,如下圖所示。

  1. 獲取用戶信息之後,才能調用商品詳情和物流信息接口。
  2. 成功獲取商品詳情和物流信息之後,才能調用商品推薦接口。

對於 Java 程序來說,Java 8 才被引入的 CompletableFuture 可以幫助我們來做多個任務的編排,功能非常強大。

這篇文章是 CompletableFuture 的簡單入門,帶大家看看 CompletableFuture 常用的 API。

Future 介紹

Future 類是異步思想的典型運用,主要用在一些需要執行耗時任務的場景,避免程序一直原地等待耗時任務執行完成,執行效率太低。具體來說是這樣的:當我們執行某一耗時的任務時,可以將這個耗時任務交給一個子線程去異步執行,同時我們可以乾點其他事情,不用傻傻等待耗時任務執行完成。等我們的事情幹完後,我們再通過 Future 類獲取到耗時任務的執行結果。這樣一來,程序的執行效率就明顯提高了。

這其實就是多線程中經典的 Future 模式,你可以將其看作是一種設計模式,核心思想是異步調用,主要用在多線程領域,並非 Java 語言獨有。

在 Java 中,Future 類只是一個泛型接口,位於 java.util.concurrent 包下,其中定義了 5 個方法,主要包括下面這 4 個功能:

  • 取消任務;
  • 判斷任務是否被取消;
  • 判斷任務是否已經執行完成;
  • 獲取任務執行結果。
// V 代表了Future執行的任務返回值的類型
public interface Future<V> {
    // 取消任務執行
    // 成功取消返回 true,否則返回 false
    boolean cancel(boolean mayInterruptIfRunning);
    // 判斷任務是否被取消
    boolean isCancelled();
    // 判斷任務是否已經執行完成
    boolean isDone();
    // 獲取任務執行結果
    V get() throws InterruptedException, ExecutionException;
    // 指定時間內沒有返回計算結果就拋出 TimeOutException 異常
    V get(long timeout, TimeUnit unit)

        throws InterruptedException, ExecutionException, TimeoutExceptio

}

簡單理解就是:我有一個任務,提交給了 Future 來處理。任務執行期間我自己可以去做任何想做的事情。並且,在這期間我還可以取消任務以及獲取任務的執行狀態。一段時間之後,我就可以 Future 那裏直接取出任務執行結果。

CompletableFuture 介紹

Future 在實際使用過程中存在一些侷限性比如不支持異步任務的編排組合、獲取計算結果的 get() 方法爲阻塞調用。

Java 8 才被引入CompletableFuture 類可以解決Future 的這些缺陷。CompletableFuture 除了提供了更爲好用和強大的 Future 特性之外,還提供了函數式編程、異步任務編排組合(可以將多個異步任務串聯起來,組成一個完整的鏈式調用)等能力。

下面我們來簡單看看 CompletableFuture 類的定義。

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

可以看到,CompletableFuture 同時實現了 FutureCompletionStage 接口。

CompletionStage 接口描述了一個異步計算的階段。很多計算可以分成多個階段或步驟,此時可以通過它將所有步驟組合起來,形成異步計算的流水線。

CompletableFuture 除了提供了更爲好用和強大的 Future 特性之外,還提供了函數式編程的能力。

Future 接口有 5 個方法:

  • boolean cancel(boolean mayInterruptIfRunning):嘗試取消執行任務。
  • boolean isCancelled():判斷任務是否被取消。
  • boolean isDone():判斷任務是否已經被執行完成。
  • get():等待任務執行完成並獲取運算結果。
  • get(long timeout, TimeUnit unit):多了一個超時時間。

CompletionStage 接口描述了一個異步計算的階段。很多計算可以分成多個階段或步驟,此時可以通過它將所有步驟組合起來,形成異步計算的流水線。

CompletionStage 接口中的方法比較多,CompletableFuture 的函數式能力就是這個接口賦予的。從這個接口的方法參數你就可以發現其大量使用了 Java8 引入的函數式編程。

由於方法衆多,所以這裏不能一一講解,下文中我會介紹大部分常見方法的使用。

CompletableFuture 常見操作

創建 CompletableFuture

常見的創建 CompletableFuture 對象的方法如下:

  1. 通過 new 關鍵字。
  2. 基於 CompletableFuture 自帶的靜態工廠方法:runAsync()supplyAsync()

new 關鍵字

通過 new 關鍵字創建 CompletableFuture 對象這種使用方式可以看作是將 CompletableFuture 當做 Future 來使用。

我在我的開源項目 guide-rpc-framework 中就是這種方式創建的 CompletableFuture 對象。

下面咱們來看一個簡單的案例。

我們通過創建了一個結果值類型爲 RpcResponse<Object>CompletableFuture,你可以把 resultFuture 看作是異步運算結果的載體。

CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();

假設在未來的某個時刻,我們得到了最終的結果。這時,我們可以調用 complete() 方法爲其傳入結果,這表示 resultFuture 已經被完成了。

// complete() 方法只能調用一次,後續調用將被忽略。
resultFuture.complete(rpcResponse);

你可以通過 isDone() 方法來檢查是否已經完成。

public boolean isDone() {
    return result != null;
}

獲取異步計算的結果也非常簡單,直接調用 get() 方法即可。調用 get() 方法的線程會阻塞直到 CompletableFuture 完成運算。

rpcResponse = completableFuture.get();

如果你已經知道計算的結果的話,可以使用靜態方法 completedFuture() 來創建 CompletableFuture

CompletableFuture<String> future = CompletableFuture.completedFuture("hello!");
assertEquals("hello!", future.get());

completedFuture() 方法底層調用的是帶參數的 new 方法,只不過,這個方法不對外暴露。

public static <U> CompletableFuture<U> completedFuture(U value) {
    return new CompletableFuture<U>((value == null) ? NIL : value);
}

靜態工廠方法

這兩個方法可以幫助我們封裝計算邏輯。

static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
// 使用自定義線程池(推薦)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);
static CompletableFuture<Void> runAsync(Runnable runnable);
// 使用自定義線程池(推薦)
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);

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

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

supplyAsync() 方法接受的參數是 Supplier<U> ,這也是一個函數式接口,U 是返回結果值的類型。

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

當你需要異步操作且關心返回結果的時候,可以使用 supplyAsync() 方法。

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> System.out.println("hello!"));
future.get();// 輸出 "hello!"
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "hello!");
assertEquals("hello!", future2.get());

處理異步結算的結果

當我們獲取到異步計算的結果之後,還可以對其進行進一步的處理,比較常用的方法有下面幾個:

  • thenApply()
  • thenAccept()
  • thenRun()
  • whenComplete()

thenApply() 方法接受一個 Function 實例,用它來處理結果。

// 沿用上一個任務的線程池
public <U> CompletableFuture<U> thenApply(
    Function<? super T,? extends U> fn) {
    return uniApplyStage(null, fn);
}

//使用默認的 ForkJoinPool 線程池(不推薦)
public <U> CompletableFuture<U> thenApplyAsync(
    Function<? super T,? extends U> fn) {
    return uniApplyStage(defaultExecutor(), fn);
}
// 使用自定義線程池(推薦)
public <U> CompletableFuture<U> thenApplyAsync(
    Function<? super T,? extends U> fn, Executor executor) {
    return uniApplyStage(screenExecutor(executor), fn);
}

thenApply() 方法使用示例如下:

CompletableFuture<String> future = CompletableFuture.completedFuture("hello!")
        .thenApply(s -> s + "world!");
assertEquals("hello!world!", future.get());
// 這次調用將被忽略。
future.thenApply(s -> s + "nice!");
assertEquals("hello!world!", future.get());

你還可以進行 流式調用

CompletableFuture<String> future = CompletableFuture.completedFuture("hello!")
        .thenApply(s -> s + "world!").thenApply(s -> s + "nice!");
assertEquals("hello!world!nice!", future.get());

如果你不需要從回調函數中獲取返回結果,可以使用 thenAccept() 或者 thenRun()。這兩個方法的區別在於 thenRun() 不能訪問異步計算的結果。

thenAccept() 方法的參數是 Consumer<? super T>

public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
    return uniAcceptStage(null, action);
}

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
    return uniAcceptStage(defaultExecutor(), action);
}

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action,
                                               Executor executor) {
    return uniAcceptStage(screenExecutor(executor), action);
}

顧名思義,Consumer 屬於消費型接口,它可以接收 1 個輸入對象然後進行“消費”。

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

thenRun() 的方法是的參數是 Runnable

public CompletableFuture<Void> thenRun(Runnable action) {
    return uniRunStage(null, action);
}

public CompletableFuture<Void> thenRunAsync(Runnable action) {
    return uniRunStage(defaultExecutor(), action);
}

public CompletableFuture<Void> thenRunAsync(Runnable action,
                                            Executor executor) {
    return uniRunStage(screenExecutor(executor), action);
}

thenAccept()thenRun() 使用示例如下:

CompletableFuture.completedFuture("hello!")
        .thenApply(s -> s + "world!").thenApply(s -> s + "nice!").thenAccept(System.out::println);//hello!world!nice!

CompletableFuture.completedFuture("hello!")
        .thenApply(s -> s + "world!").thenApply(s -> s + "nice!").thenRun(() -> System.out.println("hello!"));//hello!

whenComplete() 的方法的參數是 BiConsumer<? super T, ? super Throwable>

public CompletableFuture<T> whenComplete(
    BiConsumer<? super T, ? super Throwable> action) {
    return uniWhenCompleteStage(null, action);
}


public CompletableFuture<T> whenCompleteAsync(
    BiConsumer<? super T, ? super Throwable> action) {
    return uniWhenCompleteStage(defaultExecutor(), action);
}
// 使用自定義線程池(推薦)
public CompletableFuture<T> whenCompleteAsync(
    BiConsumer<? super T, ? super Throwable> action, Executor executor) {
    return uniWhenCompleteStage(screenExecutor(executor), action);
}

相對於 ConsumerBiConsumer 可以接收 2 個輸入對象然後進行“消費”。

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);

    default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
        Objects.requireNonNull(after);

        return (l, r) -> {
            accept(l, r);
            after.accept(l, r);
        };
    }
}

whenComplete() 使用示例如下:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "hello!")
        .whenComplete((res, ex) -> {
            // res 代表返回的結果
            // ex 的類型爲 Throwable ,代表拋出的異常
            System.out.println(res);
            // 這裏沒有拋出異常所有爲 null
            assertNull(ex);
        });
assertEquals("hello!", future.get());

異常處理

你可以通過 handle() 方法來處理任務執行過程中可能出現的拋出異常的情況。

public <U> CompletableFuture<U> handle(
    BiFunction<? super T, Throwable, ? extends U> fn) {
    return uniHandleStage(null, fn);
}

public <U> CompletableFuture<U> handleAsync(
    BiFunction<? super T, Throwable, ? extends U> fn) {
    return uniHandleStage(defaultExecutor(), fn);
}

public <U> CompletableFuture<U> handleAsync(
    BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) {
    return uniHandleStage(screenExecutor(executor), fn);
}

示例代碼如下:

CompletableFuture<String> future
        = CompletableFuture.supplyAsync(() -> {
    if (true) {
        throw new RuntimeException("Computation error!");
    }
    return "hello!";
}).handle((res, ex) -> {
    // res 代表返回的結果
    // ex 的類型爲 Throwable ,代表拋出的異常
    return res != null ? res : "world!";
});
assertEquals("world!", future.get());

你還可以通過 exceptionally() 方法來處理異常情況。

CompletableFuture<String> future
        = CompletableFuture.supplyAsync(() -> {
    if (true) {
        throw new RuntimeException("Computation error!");
    }
    return "hello!";
}).exceptionally(ex -> {
    System.out.println(ex.toString());// CompletionException
    return "world!";
});
assertEquals("world!", future.get());

如果你想讓 CompletableFuture 的結果就是異常的話,可以使用 completeExceptionally() 方法爲其賦值。

CompletableFuture<String> completableFuture = new CompletableFuture<>();
// ...
completableFuture.completeExceptionally(
  new RuntimeException("Calculation failed!"));
// ...
completableFuture.get(); // ExecutionException

組合 CompletableFuture

你可以使用 thenCompose() 按順序鏈接兩個 CompletableFuture 對象,實現異步的任務鏈。它的作用是將前一個任務的返回結果作爲下一個任務的輸入參數,從而形成一個依賴關係。

public <U> CompletableFuture<U> thenCompose(
    Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(null, fn);
}

public <U> CompletableFuture<U> thenComposeAsync(
    Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(defaultExecutor(), fn);
}

public <U> CompletableFuture<U> thenComposeAsync(
    Function<? super T, ? extends CompletionStage<U>> fn,
    Executor executor) {
    return uniComposeStage(screenExecutor(executor), fn);
}

thenCompose() 方法會使用示例如下:

CompletableFuture<String> future
        = CompletableFuture.supplyAsync(() -> "hello!")
        .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "world!"));
assertEquals("hello!world!", future.get());

在實際開發中,這個方法還是非常有用的。比如說,task1 和 task2 都是異步執行的,但 task1 必須執行完成後才能開始執行 task2(task2 依賴 task1 的執行結果)。

thenCompose() 方法類似的還有 thenCombine() 方法, 它同樣可以組合兩個 CompletableFuture 對象。

CompletableFuture<String> completableFuture
        = CompletableFuture.supplyAsync(() -> "hello!")
        .thenCombine(CompletableFuture.supplyAsync(
                () -> "world!"), (s1, s2) -> s1 + s2)
        .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "nice!"));
assertEquals("hello!world!nice!", completableFuture.get());

thenCompose()thenCombine() 有什麼區別呢?

  • thenCompose() 可以鏈接兩個 CompletableFuture 對象,並將前一個任務的返回結果作爲下一個任務的參數,它們之間存在着先後順序。
  • thenCombine() 會在兩個任務都執行完成後,把兩個任務的結果合併。兩個任務是並行執行的,它們之間並沒有先後依賴順序。

除了 thenCompose()thenCombine() 之外, 還有一些其他的組合 CompletableFuture 的方法用於實現不同的效果,滿足不同的業務需求。

例如,如果我們想要實現 task1 和 task2 中的任意一個任務執行完後就執行 task3 的話,可以使用 acceptEither()

public CompletableFuture<Void> acceptEither(
    CompletionStage<? extends T> other, Consumer<? super T> action) {
    return orAcceptStage(null, other, action);
}

public CompletableFuture<Void> acceptEitherAsync(
    CompletionStage<? extends T> other, Consumer<? super T> action) {
    return orAcceptStage(asyncPool, other, action);
}

簡單舉一個例子:

CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> {
    System.out.println("任務1開始執行,當前時間:" + System.currentTimeMillis());
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("任務1執行完畢,當前時間:" + System.currentTimeMillis());
    return "task1";
});

CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
    System.out.println("任務2開始執行,當前時間:" + System.currentTimeMillis());
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("任務2執行完畢,當前時間:" + System.currentTimeMillis());
    return "task2";
});

task.acceptEitherAsync(task2, (res) -> {
    System.out.println("任務3開始執行,當前時間:" + System.currentTimeMillis());
    System.out.println("上一個任務的結果爲:" + res);
});

// 增加一些延遲時間,確保異步任務有足夠的時間完成
try {
    Thread.sleep(2000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

輸出:

任務1開始執行,當前時間:1695088058520
任務2開始執行,當前時間:1695088058521
任務1執行完畢,當前時間:1695088059023
任務3開始執行,當前時間:1695088059023
上一個任務的結果爲:task1
任務2執行完畢,當前時間:1695088059523

任務組合操作acceptEitherAsync()會在異步任務 1 和異步任務 2 中的任意一個完成時觸發執行任務 3,但是需要注意,這個觸發時機是不確定的。如果任務 1 和任務 2 都還未完成,那麼任務 3 就不能被執行。

並行運行多個 CompletableFuture

你可以通過 CompletableFutureallOf()這個靜態方法來並行運行多個 CompletableFuture

實際項目中,我們經常需要並行運行多個互不相關的任務,這些任務之間沒有依賴關係,可以互相獨立地運行。

比說我們要讀取處理 6 個文件,這 6 個任務都是沒有執行順序依賴的任務,但是我們需要返回給用戶的時候將這幾個文件的處理的結果進行統計整理。像這種情況我們就可以使用並行運行多個 CompletableFuture 來處理。

示例代碼如下:

CompletableFuture<Void> task1 =
  CompletableFuture.supplyAsync(()->{
    //自定義業務操作
  });
......
CompletableFuture<Void> task6 =
  CompletableFuture.supplyAsync(()->{
    //自定義業務操作
  });
......
 CompletableFuture<Void> headerFuture=CompletableFuture.allOf(task1,.....,task6);

  try {
    headerFuture.join();
  } catch (Exception ex) {
    ......
  }
System.out.println("all done. ");

經常和 allOf() 方法拿來對比的是 anyOf() 方法。

allOf() 方法會等到所有的 CompletableFuture 都運行完成之後再返回

Random rand = new Random();
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000 + rand.nextInt(1000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("future1 done...");
    }
    return "abc";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000 + rand.nextInt(1000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("future2 done...");
    }
    return "efg";
});

調用 join() 可以讓程序等future1future2 都運行完了之後再繼續執行。

CompletableFuture<Void> completableFuture = CompletableFuture.allOf(future1, future2);
completableFuture.join();
assertTrue(completableFuture.isDone());
System.out.println("all futures done...");

輸出:

future1 done...
future2 done...
all futures done...

anyOf() 方法不會等待所有的 CompletableFuture 都運行完成之後再返回,只要有一個執行完成即可!

CompletableFuture<Object> f = CompletableFuture.anyOf(future1, future2);
System.out.println(f.get());

輸出結果可能是:

future2 done...
efg

也可能是:

future1 done...
abc

CompletableFuture 使用建議

使用自定義線程池

我們上面的代碼示例中,爲了方便,都沒有選擇自定義線程池。實際項目中,這是不可取的。

CompletableFuture 默認使用ForkJoinPool.commonPool() 作爲執行器,這個線程池是全局共享的,可能會被其他任務佔用,導致性能下降或者飢餓。因此,建議使用自定義的線程池來執行 CompletableFuture 的異步任務,可以提高併發度和靈活性。

private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>());

CompletableFuture.runAsync(() -> {
 		//...
}, executor);

儘量避免使用 get()

CompletableFutureget()方法是阻塞的,儘量避免使用。如果必須要使用的話,需要添加超時時間,否則可能會導致主線程一直等待,無法執行其他任務。

    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Hello, world!";
    });

    // 獲取異步任務的返回值,設置超時時間爲 5 秒
    try {
        String result = future.get(5, TimeUnit.SECONDS);
        System.out.println(result);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        // 處理異常
        e.printStackTrace();
    }
}

上面這段代碼在調用 get() 時拋出了 TimeoutException 異常。這樣我們就可以在異常處理中進行相應的操作,比如取消任務、重試任務、記錄日誌等。

正確進行異常處理

使用 CompletableFuture的時候一定要以正確的方式進行異常處理,避免異常丟失或者出現不可控問題。

下面是一些建議:

  • 使用 whenComplete 方法可以在任務完成時觸發回調函數,並正確地處理異常,而不是讓異常被吞噬或丟失。
  • 使用 exceptionally 方法可以處理異常並重新拋出,以便異常能夠傳播到後續階段,而不是讓異常被忽略或終止。
  • 使用 handle 方法可以處理正常的返回結果和異常,並返回一個新的結果,而不是讓異常影響正常的業務邏輯。
  • 使用 CompletableFuture.allOf 方法可以組合多個 CompletableFuture,並統一處理所有任務的異常,而不是讓異常處理過於冗長或重複。
  • ......

合理組合多個異步任務

正確使用 thenCompose()thenCombine()acceptEither()allOf()anyOf() 等方法來組合多個異步任務,以滿足實際業務的需求,提高程序執行效率。

實際使用中,我們還可以利用或者參考現成的異步任務編排框架,比如京東的 asyncTool

asyncTool README 文檔

後記

這篇文章只是簡單介紹了 CompletableFuture 比較常用的一些 API 。如果想要深入學習的話,還可以多找一些書籍和博客看,比如下面幾篇文章就挺不錯:

另外,建議 G 友們可以看看京東的 asyncTool 這個併發框架,裏面大量使用到了 CompletableFuture

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