【Java 8】FutureTask、CompletableFuture實踐案例

1. 前言

Java 8 更新了很多特性,其中 Lambda、stream API 等,其中 CompletableFuture 也是 Java 8 新特性之一,旨在對 Java 異步編程提供良好的 API。本文主要講解 CompletableFuture 的簡單使用。如果想了解其原理,請移步文末 “參考”。

2. demo 代碼

首先,我們先創建 demo 代碼。當然,完全同步執行。

依賴,jdk 1.8

2.1. 定義耗時操作

/**
 * 生產者,比作需要提前進行執行的操作
 * @return
 */
public int provider() throws InterruptedException {
    log.info("provider time={}", System.currentTimeMillis());
    // 模擬耗時操作
    Thread.sleep(3000);
    return 1;
}

/**
 * 消費者,耗時操作
 */
public int consumer(int message) throws InterruptedException {
    log.info("consumer time={}",System.currentTimeMillis());
    // 模擬耗時操作
    Thread.sleep(3000);
    log.info("consumer message={}, time={}",message, System.currentTimeMillis());
    return message * 2;
}

2.2. 同步代碼

進行兩次生產兩次消費

/**
 * 同步版本
 */
@Test
public void sync() throws InterruptedException {
    long startTime = System.currentTimeMillis();

    int oneProviderResult = provider();
    int twoProviderResult = provider();
    int oneConsumerResult = consumer(oneProviderResult);
    int twoConsumerResult = consumer(twoProviderResult);
    log.info("sync: one={}, two={}", oneConsumerResult, twoConsumerResult);

    long endTime = System.currentTimeMillis();
    log.info("sync time={}", (endTime - startTime));
}

2.2.1. 運行日誌

00:21:18.230 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - provider time=1569342078225
00:21:21.242 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - provider time=1569342081242
00:21:24.243 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer time=1569342084243
00:21:27.243 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer message=1, time=1569342087243
00:21:27.243 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer time=1569342087243
00:21:30.244 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer message=1, time=1569342090244
00:21:30.244 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - sync: one=2, two=2
00:21:30.244 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - sync time=12019

日誌分析:

從日誌中可以看出,先進行兩次 provider, 然後兩次 consumer , 然後打印結果、耗時。

程序正常運行,總耗時爲 12019 ms,即 兩次 provider 6000ms,兩次 consumer 6000ms,加上其它開銷可忽略不計。

2.3. 同步代碼另一種寫法

當然,滿足需求的代碼,還有另一種更爲簡潔的寫法,幾乎不需要中間變量。

/**
 * 同步版本
 */
@Test
public void syncOther() throws InterruptedException {
    long startTime = System.currentTimeMillis();

    log.info("syncOther: one={}, two={}",consumer(provider()), consumer(provider()));

    log.info("syncOther: time={}", System.currentTimeMillis() - startTime);
}

2.3.1. 運行日誌

00:29:14.839 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - provider time=1569342554835
00:29:17.866 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer time=1569342557866
00:29:20.867 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer message=1, time=1569342560867
00:29:20.867 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - provider time=1569342560867
00:29:23.867 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer time=1569342563867
00:29:26.884 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer message=1, time=1569342566884
00:29:26.884 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - syncOther: one=2, two=2
00:29:26.884 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - syncOther: time=12049

日誌分析:

從日誌中看出,先進行 provider, 然後一次 consumer, 然後一次 provider,然後一次 consumer,然後打印結果、耗時。

執行結果和之前版本一樣,耗時一樣(12 s),只是執行順序改變。

3. FutureTask版本

對於之前的版本,實現了功能,但對性能來講耗時較爲嚴重。

3.1. 異步分析

就該程序而言,主要流程是進行兩次 provider,每個 provider 的結果提交給 consumer,所有的耗時操作都是 3000 ms。

其實就兩次 provider 來講,目前的執行流程是串行執行,即一個 provider 執行完成後再執行另一個 provider 。從數據上來講,第二個 provider 的執行應該不依賴另一個 provider 的執行,即他們可以並行執行。

同樣的對於兩個 consumer ,假設其數據依賴都已經準備完成,兩個 consumer 同樣應該並行執行。

對於 provider 和 consumer ,其相互之間存在數據依賴,業務上來講應該串行執行。

分析得:兩個 provider 並行執行,兩個 consumer 並行執行,provider 和 consumer 串行執行。
耗時:兩個 provider 並行執行耗時 3000 ms,兩個 consumer 並行執行耗時 3000 ms,總耗時 6000 ms。

分析上來講,相較於同步版本,耗時減少一半。那我們來進行實現。

3.2. FutureTask 版本

直接上代碼

/**
 * FutureTask 版本
 */
@Test
public void futureTask() throws ExecutionException, InterruptedException {
    long startTime = System.currentTimeMillis();

    ExecutorService executor = Executors.newCachedThreadPool();
    // 創建 provider 任務
    FutureTask<Integer> oneFuture = new FutureTask<>(() -> provider());
    FutureTask<Integer> twoFuture = new FutureTask<>(() -> provider());
    // 提交 provider 任務
    executor.submit(oneFuture);
    executor.submit(twoFuture);
    // 獲取中間數據
    Integer oneProviderResult = oneFuture.get();
    Integer twoProviderResult = twoFuture.get();

    // 創建 consumer 任務
    FutureTask<Integer> threeFuture = new FutureTask<>(() -> consumer(oneProviderResult));
    FutureTask<Integer> fourFuture = new FutureTask<>(() -> consumer(twoProviderResult));
    // 提交 consumer 任務
    executor.submit(threeFuture);
    executor.submit(fourFuture);
    // 獲取結果
    Integer oneConsumerResult = threeFuture.get();
    Integer twoConsumerResult = fourFuture.get();

    log.info("sync: one={}, two={}", oneConsumerResult, twoConsumerResult);

    log.info("syncOther: time={}", System.currentTimeMillis() - startTime);
}

代碼說明:

  • 此處採用了 Executors.newCachedThreadPool() 線程池,並採用默認配置。實際項目中,建議自定義線程池或者進行參數配置。
  • 在 consumer 中直接使用了全局變量(中間數據結果),建議實際項目中不採用這種寫法。
  • 每一個任務流程:創建任務、提交任務、任務執行、獲取任務結果

3.2.1. 日誌分析

00:58:13.849 [pool-1-thread-1] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - provider time=1569344293845
00:58:13.849 [pool-1-thread-2] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - provider time=1569344293848
00:58:16.856 [pool-1-thread-2] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer time=1569344296856
00:58:16.856 [pool-1-thread-1] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer time=1569344296856
00:58:19.856 [pool-1-thread-2] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer message=1, time=1569344299856
00:58:19.856 [pool-1-thread-1] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer message=1, time=1569344299856
00:58:19.856 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - sync: one=2, two=2
00:58:19.856 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - syncOther: time=6117

日誌分析:

可以看到,先執行兩個 provider,然後兩個 consumer ,然後打印結果、耗時。

似乎和之前的同步版本沒什麼區別。我們細細觀察,總耗時 6117 ms , 減少了一半。和我們之前分析結果相符。

再細細觀察,兩個 provider 打印時間幾乎相同、兩個 consumer 打印時間完全相同。可以看出,兩個 provider 並行執行,兩個 consumer 並行執行。和我們的預期相符。

3.2.2. 異步分析

從 FutureTask 代碼中可以看出,我們進行了 provider 創建任務後,手動進行了提交任務,然後手動進行了 get (此處的 get() 是一個阻塞操作),獲取到數據後才進行 consumer 任務的創建、提交、運行、獲取結果。

那麼,可以讓這一切自動化麼?即,provider 創建任務、提交執行、執行、獲取結果,提交給 consumer 任務執行。且看下文 CompletableFuture (這是正菜)。

4. CompletableFuture

廢話不多說,直接上代碼:

/**
 * CompletableFuture 版本
 */
@Test
public void completableFutureTest() throws ExecutionException, InterruptedException {
    long startTime = System.currentTimeMillis();

    ExecutorService executor = Executors.newCachedThreadPool();
    CompletableFuture<Integer> one = CompletableFuture
            .supplyAsync(() -> {
                try {
                    return provider();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return 0;
                }
            }, executor)
            .thenApplyAsync((res) -> {
                try {
                    return consumer(res);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return 0;
                }
            }, executor);

    CompletableFuture<Integer> two = CompletableFuture
            .supplyAsync(() -> {
                try {
                    return provider();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return 0;
                }
            }, executor)
            .thenApplyAsync((res) -> {
                try {
                    return consumer(res);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return 0;
                }
            }, executor);

    /**
     * 等待執行,需要等待 one, two 兩個任務均執行完畢
     */
    CompletableFuture.allOf(one, two);

    log.info("sync: one={}, two={}", one.get(), two.get());

    log.info("syncOther: time={}", System.currentTimeMillis() - startTime);
}

4.1. 日誌分析

01:19:24.956 [pool-1-thread-2] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - provider time=1569345564953
01:19:24.956 [pool-1-thread-1] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - provider time=1569345564953
01:19:27.965 [pool-1-thread-3] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer time=1569345567965
01:19:27.971 [pool-1-thread-2] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer time=1569345567971
01:19:30.966 [pool-1-thread-3] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer message=1, time=1569345570966
01:19:30.971 [pool-1-thread-2] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - consumer message=1, time=1569345570971
01:19:30.971 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - sync: one=2, two=2
01:19:30.971 [main] INFO club.chenlinghong.demo.javaeight.completablefuture.CompletableFutureDemo - syncOther: time=6201

同 FutureTask 版本,兩個 provider 異步執行耗時 3000 ms, 兩個 consumer 異步執行耗時 3000 ms ,總耗時 6000 ms。

其它耗時如線程池創建、線程創建、賦值操作等可忽略不計。

4.2. FutureTask VS CompletableFuture

兩個異步版本對比來看,對於當前實例,差別不大。

CompletableFuture 相較於 FutureTask ,此實踐使用到的是 主動計算 的特性,多個異步任務之間依賴執行,自動通知下一個任務執行。

CompletableFuture 相較於 FutureTask ,還使用到了多個任務編排,此實踐使用的是 allOf(),即多個任務都必須執行完畢。較長用於最後進行等待。另外,還有比如 any 之類的編排 API 。

4.3. 簡化代碼

當然,上述 CompletableFuture 代碼太過冗餘,可以簡化下。

// 公共
ExecutorService executor = Executors.newCachedThreadPool();

/**
 * CompletableFuture 版本
 */
@Test
public void completableFutureTest_01() throws ExecutionException, InterruptedException {
    long startTime = System.currentTimeMillis();

    CompletableFuture<Integer> one = completableFutureExecute();
    CompletableFuture<Integer> two = completableFutureExecute();
    /**
     * 等待執行,需要等待 one, two 兩個任務均執行完畢
     */
    CompletableFuture.allOf(one, two);
    log.info("sync: one={}, two={}", one.get(), two.get());

    log.info("syncOther: time={}", System.currentTimeMillis() - startTime);
}

// 抽象
private CompletableFuture<Integer> completableFutureExecute() {
    return CompletableFuture
            .supplyAsync(() -> {
                try {
                    return provider();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return 0;
                }
            }, executor)
            .thenApplyAsync((res) -> {
                try {
                    return consumer(res);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return 0;
                }
            }, executor);
}

5. 完整代碼

https://github.com/lambochen/demo

6. 參考

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