多執行緒系列(二十) -CompletableFuture使用詳解

程序员志哥發表於2024-03-15

一、摘要

在上篇文章中,我們介紹了Future相關的用法,使用它可以獲取非同步任務執行的返回值。

我們再次回顧一下Future相關的用法。

public class FutureTest {

    public static void main(String[] args) throws Exception {
        long startTime = System.currentTimeMillis();
        // 建立一個執行緒池
        ExecutorService executor = Executors.newFixedThreadPool(1);

        // 提交任務並獲得Future的例項
        Future<String> future = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                // 執行下載某檔案任務,並返回檔名稱
                System.out.println("thread name:" +  Thread.currentThread().getName() + " 開始執行下載任務");
                Thread.sleep(200);
                return "xxx.png";
            }
        });

        //模擬主執行緒其它操作耗時
        Thread.sleep(300);

        // 透過阻塞方式,從Future中獲取非同步執行返回的結果
        String result = future.get();
        System.out.println("任務執行結果:" +  result);
        System.out.println("總共用時:" + (System.currentTimeMillis() - startTime) + "ms");

        // 任務執行完畢之後,關閉執行緒池
        executor.shutdown();
    }
}

執行結果如下:

thread name:pool-1-thread-1 開始執行下載任務
任務執行結果:xxx.png
總共用時:308ms

如果不採用執行緒執行,那麼總共用時應該會是 200 + 300 = 500 ms,而採用執行緒來非同步執行,總共用時是 308 ms。不難發現,透過Future和執行緒池的搭配使用,可以有效的提升程式的執行效率。

但是Future對非同步執行結果的獲取並不是很友好,要麼呼叫阻塞方法get()獲取結果,要麼輪訓呼叫isDone()方法是否等於true來判斷任務是否執行完畢來獲取結果,這兩種方法都不算很好,因為主執行緒會被迫等待。

因此,從 Java 8 開始引入了CompletableFuture,它針對Future做了很多的改進,在實現Future介面相關功能之外,還支援傳入回撥物件,當非同步任務完成或者發生異常時,自動呼叫回撥物件方法。

下面我們一起來看看CompletableFuture相關的用法!

二、CompletableFuture 用法介紹

我們還是以上面的例子為例,改用CompletableFuture來實現,內容如下:

public class FutureTest2 {

    public static void main(String[] args) throws Exception {
        // 建立非同步執行任務
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(FutureTest2::download);

        // 如果執行成功,回撥此方法
        cf.thenAccept((result) -> {
            System.out.println("任務執行成功,返回結果值:" +  result);
        });

        // 如果執行異常,回撥此方法
        cf.exceptionally((e) -> {
            System.out.println("任務執行失敗,原因:" +  e.getMessage());
            return null;
        });

        //模擬主執行緒其它操作耗時
        Thread.sleep(300);
    }

    /**
     * 下載某個任務
     * @return
     */
    private static String download(){
        // 執行下載某檔案任務,並返回檔名稱
        System.out.println("thread name:" +  Thread.currentThread().getName() + " 開始執行下載任務");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "xxx.png";
    }
}

執行結果如下:

thread name:ForkJoinPool.commonPool-worker-1 開始執行下載任務
任務執行成功,返回結果值:xxx.png

可以發現,採用CompletableFuture類的supplyAsync()方法進行非同步程式設計,程式碼上簡潔了很多,不需要單獨建立執行緒池。

實際上,CompletableFuture也使用了執行緒池來執行任務,部分核心原始碼如下:

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

    // 判斷當前機器 cpu 可用邏輯核心數是否大於1
    private static final boolean useCommonPool = (ForkJoinPool.getCommonPoolParallelism() > 1);
    
    // 預設採用的執行緒池
    // 如果useCommonPool = true,採用 ForkJoinPool.commonPool 執行緒池
    // 如果useCommonPool = false,採用 ThreadPerTaskExecutor 執行器
    private static final Executor asyncPool = useCommonPool ?
        ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

    // ThreadPerTaskExecutor執行器類
    static final class ThreadPerTaskExecutor implements Executor {
        public void execute(Runnable r) { new Thread(r).start(); }
    }


    // 非同步執行任務的方法
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
        return asyncSupplyStage(asyncPool, supplier);
    }

    // 非同步執行任務的方法,支援傳入自定義執行緒池
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                       Executor executor) {
        return asyncSupplyStage(screenExecutor(executor), supplier);
    }
}

從原始碼上可以分析出如下幾點:

  • 當前機器 cpu 可用邏輯核心數大於 1,預設會採用ForkJoinPool.commonPool()執行緒池來執行任務
  • 當前機器 cpu 可用邏輯核心數等於 1,預設會採用ThreadPerTaskExecutor類來執行任務,它是個一對一執行器,每提交一個任務會建立一個新的執行緒來執行
  • 同時也支援使用者傳入自定義執行緒池來非同步執行任務

其中ForkJoinPool執行緒池是從 JDK 1.7 版本引入的,它是一個全新的執行緒池,後面在介紹Fork/Join框架文章中對其進行介紹。

除此之外,CompletableFuture為開發者還提供了幾十種方法,以便滿足更多的非同步任務執行的場景。這些方法包括建立非同步任務、任務非同步回撥、多個任務組合處理等內容,下面我們就一起來學習一下相關的使用方式。

2.1、建立非同步任務

CompletableFuture建立非同步任務,常用的方法有兩個。

  • runAsync():執行非同步任務時,沒有返回值
  • supplyAsync():執行非同步任務時,可以帶返回值

runAsync()supplyAsync()方法相關的原始碼如下:

// 使用預設內建執行緒池執行任務,根據runnable構建執行任務,無返回值
public static CompletableFuture<Void> runAsync(Runnable runnable)

// 使用自定義執行緒池執行任務,根據runnable構建執行任務,無返回值
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)

// 使用預設內建執行緒池執行任務,根據supplyAsync構建執行任務,可以帶返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

// 使用自定義執行緒池執行任務,根據supplyAsync構建執行任務,可以帶返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

兩者都支援使用自定義的執行緒池來執行任務,稍有不同的是supplyAsync()方法的入參使用的是Supplier介面,它表示結果的提供者,該結果返回一個物件且不接受任何引數,支援透過 lambda 語法簡寫

下面我們一起來看看相關的使用示例!

2.1.1、runAsync 使用示例
public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<Void> cf = CompletableFuture.runAsync(new Runnable() {
        @Override
        public void run() {
            System.out.println("runAsync,執行完畢");
        }
    });
    System.out.println("runAsync,任務執行結果:" + cf.get());
}

輸出結果:

runAsync,執行完畢
runAsync,任務執行結果:null
2.1.2、supplyAsync 使用示例
public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync,執行完畢");
        return "hello world";
    });
    System.out.println("supplyAsync,任務執行結果:" + cf.get());
}

輸出結果:

supplyAsync,執行完畢
supplyAsync,任務執行結果:hello world

2.2、任務非同步回撥

當建立的非同步任務執行完畢之後,我們希望拿著上一個任務的執行結果,繼續執行後續的任務,此時就可以採用回撥方法來處理。

CompletableFuture針對任務非同步回撥做了很多的支援,常用的方法如下:

  • thenRun()/thenRunAsync():它表示上一個任務執行成功後的回撥方法,無入參,無返回值
  • thenAccept()/thenAcceptAsync():它表示上一個任務執行成功後的回撥方法,有入參,無返回值
  • thenApply()/thenApplyAsync():它表示上一個任務執行成功後的回撥方法,有入參,有返回值
  • whenComplete()/whenCompleteAsync():它表示任務執行完成後的回撥方法,有入參,無返回值
  • handle()/handleAsync():它表示任務執行完成後的回撥方法,有入參,有返回值
  • exceptionally():它表示任務執行異常後的回撥方法

下面我們一起來看看相關的使用示例!

2.2.1、thenRun/thenRunAsync

thenRun()/thenRunAsync()方法,都表示上一個任務執行成功後的回撥處理,無入參,無返回值。稍有不同的是,thenRunAsync()方法會採用獨立的執行緒池來執行任務。

相關的原始碼方法如下:

// 預設執行緒池
private static final Executor asyncPool = useCommonPool ?
        ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

// 採用與上一個任務的執行緒池來執行任務
public CompletableFuture<Void> thenRun(Runnable action) {
    return uniRunStage(null, action);
}

// 採用預設執行緒池來執行任務
public CompletableFuture<Void> thenRunAsync(Runnable action) {
    return uniRunStage(asyncPool, action);
}

從原始碼上可以清晰的看到,thenRun()/thenRunAsync()方法都呼叫了uniRunStage()方法,不同的是thenRunAsync()使用了asyncPool引數,也就是預設的執行緒池;而thenRun()方法使用的是null,底層採用上一個任務的執行緒池來執行,總結下來就是:

  • 當呼叫thenRun()方法執行任務時,當前任務和上一個任務都共用同一個執行緒池
  • 當呼叫thenRunAsync()方法執行任務時,上一個任務採用自己的執行緒池來執行;而當前任務會採用預設執行緒池來執行,比如ForkJoinPool

thenAccept()/thenAcceptAsync()thenApply()/thenApplyAsync()whenComplete()/whenCompleteAsync()handle()/handleAsync()方法之間的區別也類似,下文不再重複講解。

下面我們一起來看看thenRun()方法的使用示例。

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync,執行完畢");
        return "hello world";
    });

    // 當上一個任務執行成功,會繼續回撥當前方法
    CompletableFuture<Void> cf2 = cf1.thenRun(() -> {
        System.out.println("thenRun1,執行完畢");

    });

    CompletableFuture<Void> cf3 = cf2.thenRun(() -> {
        System.out.println("thenRun2,執行完畢");
    });


    System.out.println("任務執行結果:" + cf3.get());
}

輸出結果:

supplyAsync,執行完畢
thenRun1,執行完畢
thenRun2,執行完畢
任務執行結果:null

如果上一個任務執行異常,是不會回撥thenRun()方法的,示例如下:

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync,執行完畢");
        if(1 == 1){
            throw new RuntimeException("執行異常");
        }
        return "hello world";
    });

    // 當上一個任務執行成功,會繼續回撥當前方法
    CompletableFuture<Void> cf1 = cf.thenRun(() -> {
        System.out.println("thenRun1,執行完畢");

    });

    // 監聽執行時異常的回撥方法
    CompletableFuture<Void> cf2 = cf1.exceptionally((e) -> {
        System.out.println("發生異常,錯誤資訊:" + e.getMessage());
        return null;
    });

    System.out.println("任務執行結果:" + cf2.get());
}

輸出結果:

supplyAsync,執行完畢
發生異常,錯誤資訊:java.lang.RuntimeException: 執行異常
任務執行結果:null

可以清晰的看到,thenRun()方法沒有回撥。

thenAccept()thenAcceptAsync()thenApply()thenApplyAsync()方法也類似,當上一個任務執行異常,不會回撥這些方法。

2.2.2、thenAccept/thenAcceptAsync

thenAccept()/thenAcceptAsync()方法,表示上一個任務執行成功後的回撥方法,有入參,無返回值。

相關的示例如下。

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync,執行完畢");
        return "hello world";
    });

    // 當上一個任務執行成功,會繼續回撥當前方法
    CompletableFuture<Void> cf2 = cf1.thenAccept((r) -> {
        System.out.println("thenAccept,執行完畢,上一個任務執行結果值:" + r);

    });

    System.out.println("任務執行結果:" + cf2.get());
}

輸出結果:

supplyAsync,執行完畢
thenAccept,執行完畢,上一個任務執行結果值:hello world
任務執行結果:null
2.2.3、thenApply/thenApplyAsync

thenApply()/thenApplyAsync()方法,表示上一個任務執行成功後的回撥方法,有入參,有返回值。

相關的示例如下。

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync,執行完畢");
        return "hello world";
    });

    // 當上一個任務執行成功,會繼續回撥當前方法
    CompletableFuture<String> cf2 = cf1.thenApply((r) -> {
        System.out.println("thenApply,執行完畢,上一個任務執行結果值:" + r);
        return "gogogo";
    });

    System.out.println("任務執行結果:" + cf2.get());
}

輸出結果:

supplyAsync,執行完畢
thenApply,執行完畢,上一個任務執行結果值:hello world
任務執行結果:gogogo
2.2.4、whenComplete/whenCompleteAsync

whenComplete()/whenCompleteAsync()方法,表示任務執行完成後的回撥方法,有入參,無返回值。

稍有不同的是:無論任務執行成功還是失敗,它都會回撥。

相關的示例如下。

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync,執行完畢");
        if(1 == 1){
            throw new RuntimeException("執行異常");
        }
        return "hello world";
    });

    // 當任務執行完成,會繼續回撥當前方法
    CompletableFuture<String> cf2 = cf1.whenComplete((r, e) -> {
        System.out.println("whenComplete,執行完畢,上一個任務執行結果值:" + r + ",異常資訊:" + e.getMessage());
    });

    // 監聽執行時異常的回撥方法
    CompletableFuture<String> cf3 = cf2.exceptionally((e) -> {
        System.out.println("發生異常,錯誤資訊:" + e.getMessage());
        return e.getMessage();
    });

    System.out.println("任務執行結果:" + cf3.get());
}

輸出結果:

supplyAsync,執行完畢
whenComplete,執行完畢,上一個任務執行結果值:null,異常資訊:java.lang.RuntimeException: 執行異常
發生異常,錯誤資訊:java.lang.RuntimeException: 執行異常
任務執行結果:java.lang.RuntimeException: 執行異常
2.2.5、handle/handleAsync

handle()/handleAsync()方法,表示任務執行完成後的回撥方法,有入參,有返回值。

同樣的,無論任務執行成功還是失敗,它都會回撥。

相關的示例如下。

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync,執行完畢");
        if(1 == 1){
            throw new RuntimeException("執行異常");
        }
        return "hello world";
    });

    // 當任務執行完成,會繼續回撥當前方法
    CompletableFuture<String> cf2 = cf1.handle((r, e) -> {
        System.out.println("handle,執行完畢,上一個任務執行結果值:" + r + ",異常資訊:" + e.getMessage());
        return "handle";
    });
    
    System.out.println("任務執行結果:" + cf2.get());
}

輸出結果:

supplyAsync,執行完畢
handle,執行完畢,上一個任務執行結果值:null,異常資訊:java.lang.RuntimeException: 執行異常
任務執行結果:handle
2.2.6、exceptionally

exceptionally()方法,表示任務執行異常後的回撥方法。在上文的示例中有所介紹。

最後我們還是簡單的看下示例。

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync,執行開始");
        if(1 == 1){
            throw new RuntimeException("執行異常");
        }
        return "hello world";
    });

    // 監聽執行時異常的回撥方法
    CompletableFuture<String> cf2 = cf1.exceptionally((e) -> {
        System.out.println("發生異常,錯誤資訊:" + e.getMessage());
        return e.getMessage();
    });

    System.out.println("任務執行結果:" + cf2.get());
}

輸出結果:

supplyAsync,執行開始
發生異常,錯誤資訊:java.lang.RuntimeException: 執行異常
任務執行結果:java.lang.RuntimeException: 執行異常

2.3、多個任務組合處理

某些場景下,如果希望獲取兩個不同的非同步執行結果進行組合處理,可以採用多個任務組合處理方式。

CompletableFuture針對多個任務組合處理做了很多的支援,常用的組合方式有以下幾種。

  • AND組合:表示將兩個CompletableFuture任務組合起來,只有這兩個任務都正常執行完了,才會繼續執行回撥任務,比如thenCombine()方法
  • OR組合:表示將兩個CompletableFuture任務組合起來,只要其中一個正常執行完了,就會繼續執行回撥任務,比如applyToEither方法
  • AllOf組合:可以將多個CompletableFuture任務組合起來,只有所有的任務都正常執行完了,才會繼續執行回撥任務,比如allOf()方法
  • AnyOf組合:可以將多個CompletableFuture任務組合起來,只要其中一個任務正常執行完了,就會繼續執行回撥任務,比如anyOf()方法

下面我們一起來看看相關的使用示例!

2.3.1、AND組合

實現AND組合的操作方法有很多,比如runAfterBoth()thenAcceptBoth()thenCombine()等方法,它們之間的區別在於:是否帶有入參、是否帶有返回值。

其中thenCombine()方法支援傳入參、帶返回值。

相關示例如下:

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync1,執行完畢");
        return "supplyAsync1";
    });

    CompletableFuture<String> cf2 = CompletableFuture
            .supplyAsync(() -> {
                System.out.println("supplyAsync2,執行完畢");
                return "supplyAsync2";
            })
            .thenCombine(cf1, (r1, r2) -> {
                System.out.println("r1任務執行結果:" + r1);
                System.out.println("r2任務執行結果:" + r2);
                return r1 + "_" + r2;
            });

    System.out.println("任務執行結果:" + cf2.get());
}

輸出結果:

supplyAsync1,執行完畢
supplyAsync2,執行完畢
r1任務執行結果:supplyAsync2
r2任務執行結果:supplyAsync1
任務執行結果:supplyAsync2_supplyAsync1
2.3.2、OR組合

實現OR組合的操作方法有很多,比如runAfterEither()acceptEither()applyToEither()等方法,區別同上。

其中applyToEither()方法支援傳入參、帶返回值。

相關示例如下:

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync1,執行完畢");
        return "supplyAsync1";
    });

    CompletableFuture<String> cf2 = CompletableFuture
            .supplyAsync(() -> {
                System.out.println("supplyAsync2,執行完畢");
                return "supplyAsync2";
            })
            .applyToEither(cf1, (r) -> {
                System.out.println("第一個執行成功的任務結果:" + r);
                return r + "_applyToEither";
            });

    System.out.println("任務執行結果:" + cf2.get());
}

輸出結果:

supplyAsync1,執行完畢
supplyAsync2,執行完畢
第一個執行成功的任務結果:supplyAsync2
任務執行結果:supplyAsync2_applyToEither
2.3.2、AllOf組合

實現AllOf組合的操作就一個方法allOf(),可以將多個任務進行組合,只有都執行成功才會回撥,回撥入參為空值。

相關示例如下:

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync1,執行完畢");
        return "supplyAsync1";
    });

    CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync2,執行完畢");
        return "supplyAsync2";
    });

    // 將多個任務,進行AND組合
    CompletableFuture<String> cf3 = CompletableFuture
            .allOf(cf1, cf2)
            .handle((r, e) -> {
                System.out.println("所有任務都執行成功,result:" +  r);
                return "over";
            });
    System.out.println(cf3.get());
}

輸出結果:

supplyAsync1,執行完畢
supplyAsync2,執行完畢
所有任務都執行成功,result:null
over
2.3.3、AnyOf組合

實現AnyOf組合的操作,同樣就一個方法anyOf(),可以將多個任務進行組合,只要一個執行成功就會回撥,回撥入參有值。

相關示例如下:

public static void main(String[] args) throws Exception {
    // 建立非同步執行任務
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync1,執行完畢");
        return "supplyAsync1";
    });

    CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync2,執行完畢");
        return "supplyAsync2";
    });

    // 將多個任務,進行AND組合
    CompletableFuture<String> cf3 = CompletableFuture
            .anyOf(cf1, cf2)
            .handle((r, e) -> {
                System.out.println("某個任務執行成功,返回值:" + r);
                return "over";
            });
    System.out.println(cf3.get());
}

輸出結果:

supplyAsync1,執行完畢
supplyAsync2,執行完畢
某個任務執行成功,返回值:supplyAsync1
over

三、小結

本文主要圍繞CompletableFuture類相關用法進行了一次知識總結,透過CompletableFuture類可以簡化非同步程式設計,同時支援多種非同步任務,按照條件組合處理,相比其它的併發工具類,操作更加強大、實用。

本篇內容比較多,如果有描述不對的地方,歡迎網友留言指出,希望本文知識總結能幫助到大家。

四、參考

1.https://www.liaoxuefeng.com/wiki/1252599548343744/1306581182447650

2.https://juejin.cn/post/6970558076642394142

相關文章