非同步程式設計利器:CompletableFuture

雨點的名字發表於2022-05-09
非同步程式設計利器:CompletableFuture

一、一個示例回顧Future

一些業務場景我們需要使用多執行緒非同步執行任務,加快任務執行速度。

JDK5新增了Future介面,用於描述一個非同步計算的結果。雖然 Future 以及相關使用方法提供了非同步執行任務的能力,但是對於結果的獲取卻是很不方便,

我們必須使用 **Future.get() **的方式阻塞呼叫執行緒,或者使用輪詢方式判斷 Future.isDone 任務是否結束,再獲取結果。

這兩種處理方式都不是很優雅,相關程式碼如下:

    @Test
    public void testFuture() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        Future<String> future = executorService.submit(() -> {
            Thread.sleep(2000);
            return "hello";
        });
        System.out.println(future.get());
        System.out.println("end");
    }

與此同時,Future無法解決多個非同步任務需要相互依賴的場景,簡單點說就是,主執行緒需要等待子執行緒任務執行完畢之後在進行執行,這個時候你可能想到了CountDownLatch

沒錯確實可以解決,程式碼如下。這裡定義兩個Future,第一個通過使用者id獲取使用者資訊,第二個通過商品id獲取商品資訊。

  @Test
    public void testCountDownLatch() throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        CountDownLatch downLatch = new CountDownLatch(2);
        long startTime = System.currentTimeMillis();
        Future<String> userFuture = executorService.submit(() -> {
            //模擬查詢商品耗時500毫秒
            Thread.sleep(500);
            downLatch.countDown();
            return "使用者A";
        });

        Future<String> goodsFuture = executorService.submit(() -> {
            //模擬查詢商品耗時500毫秒
            Thread.sleep(400);
            downLatch.countDown();
            return "商品A";
        });

        downLatch.await();
        //模擬主程式耗時時間
        Thread.sleep(600);
        System.out.println("獲取使用者資訊:" + userFuture.get());
        System.out.println("獲取商品資訊:" + goodsFuture.get());
        System.out.println("總共用時" + (System.currentTimeMillis() - startTime) + "ms");
    }

執行結果

獲取使用者資訊:使用者A
獲取商品資訊:商品A
總共用時1110ms

從執行結果可以看出結果都已經獲取,而且如果我們不用非同步操作,執行時間應該是:500+400+600 = 1500,用非同步操作後實際只用1110。

但是Java8以後我不在認為這是一種優雅的解決方式,接下來我們來了解下CompletableFuture的使用。


二、通過CompletableFuture實現上面示例

 @Test
    public void testCompletableInfo() throws InterruptedException, ExecutionException {
        long startTime = System.currentTimeMillis();
  
          //呼叫使用者服務獲取使用者基本資訊
          CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() ->
                  //模擬查詢商品耗時500毫秒
          {
              try {
                  Thread.sleep(500);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              return "使用者A";
          });
  
          //呼叫商品服務獲取商品基本資訊
          CompletableFuture<String> goodsFuture = CompletableFuture.supplyAsync(() ->
                  //模擬查詢商品耗時500毫秒
          {
              try {
                  Thread.sleep(400);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              return "商品A";
          });
  
          System.out.println("獲取使用者資訊:" + userFuture.get());
          System.out.println("獲取商品資訊:" + goodsFuture.get());
  
          //模擬主程式耗時時間
          Thread.sleep(600);
          System.out.println("總共用時" + (System.currentTimeMillis() - startTime) + "ms");
    }

執行結果

獲取使用者資訊:使用者A
獲取商品資訊:商品A
總共用時1112ms

通過CompletableFuture可以很輕鬆的實現CountDownLatch的功能,你以為這就結束了,遠遠不止,CompletableFuture比這要強多了。

比如可以實現:任務1執行完了再執行任務2,甚至任務1執行的結果,作為任務2的入引數等等強大功能,下面就來學學CompletableFuture的API。


三、CompletableFuture建立方式

1、常用的4種建立方式

CompletableFuture原始碼中有四個靜態方法用來執行非同步任務

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier){..}
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor){..}
public static CompletableFuture<Void> runAsync(Runnable runnable){..}
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor){..}

一般我們用上面的靜態方法來建立CompletableFuture,這裡也解釋下他們的區別:

  • supplyAsync執行任務,支援返回值。
  • runAsync執行任務,沒有返回值。

supplyAsync方法

//使用預設內建執行緒池ForkJoinPool.commonPool(),根據supplier構建執行任務
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
//自定義執行緒,根據supplier構建執行任務
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

runAsync方法

//使用預設內建執行緒池ForkJoinPool.commonPool(),根據runnable構建執行任務
public static CompletableFuture<Void> runAsync(Runnable runnable) 
//自定義執行緒,根據runnable構建執行任務
public static CompletableFuture<Void> runAsync(Runnable runnable,  Executor executor)

2、結果獲取的4種方式

對於結果的獲取CompltableFuture類提供了四種方式

//方式一
public T get()
//方式二
public T get(long timeout, TimeUnit unit)
//方式三
public T getNow(T valueIfAbsent)
//方式四
public T join()

說明

  • get()和get(long timeout, TimeUnit unit) => 在Future中就已經提供了,後者提供超時處理,如果在指定時間內未獲取結果將丟擲超時異常
  • getNow => 立即獲取結果不阻塞,結果計算已完成將返回結果或計算過程中的異常,如果未計算完成將返回設定的valueIfAbsent值
  • join => 方法裡不會丟擲異常

示例

  @Test
    public void testCompletableGet() throws InterruptedException, ExecutionException {

        CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "商品A";
        });

        // getNow方法測試 
        System.out.println(cp1.getNow("商品B"));

        //join方法測試 
        CompletableFuture<Integer> cp2 = CompletableFuture.supplyAsync((() -> 1 / 0));
        System.out.println(cp2.join());
       System.out.println("-----------------------------------------------------");
        //get方法測試
        CompletableFuture<Integer> cp3 = CompletableFuture.supplyAsync((() -> 1 / 0));
        System.out.println(cp3.get());
    }

執行結果

  • 第一個執行結果為 商品B,因為要先睡上1秒結果不能立即獲取
  • join方法獲取結果方法裡不會拋異常,但是執行結果會拋異常,丟擲的異常為CompletionException
  • get方法獲取結果方法裡將丟擲異常,執行結果丟擲的異常為ExecutionException

四、非同步回撥方法

非同步程式設計利器:CompletableFuture

1、thenRun/thenRunAsync

通俗點講就是,做完第一個任務後,再做第二個任務,這兩個任務沒有關聯關係,第二個任務也沒有返回值

示例

 @Test
    public void testCompletableThenRunAsync() throws InterruptedException, ExecutionException {
        long startTime = System.currentTimeMillis();
        
        CompletableFuture<Void> cp1 = CompletableFuture.runAsync(() -> {
            try {
                //執行任務A
                Thread.sleep(600);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });

        CompletableFuture<Void> cp2 =  cp1.thenRun(() -> {
            try {
                //執行任務B
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // get方法測試
        System.out.println(cp2.get());

        //模擬主程式耗時時間
        Thread.sleep(600);
        System.out.println("總共用時" + (System.currentTimeMillis() - startTime) + "ms");
    }
    
    //執行結果  
    /**
     *  null
     *  總共用時1610ms
     */

thenRun 和thenRunAsync有什麼區別呢?

如果你執行第一個任務的時候,傳入了一個自定義執行緒池:

  • 呼叫thenRun方法執行第二個任務時,則第二個任務和第一個任務是共用同一個執行緒池。
  • 呼叫thenRunAsync執行第二個任務時,則第一個任務使用的是你自己傳入的執行緒池,第二個任務使用的是ForkJoin執行緒池。

說明: 後面介紹的thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,它們之間的區別也是這個。

2、thenAccept/thenAcceptAsync

第一個任務執行完成後,執行第二個回撥方法任務,會將該任務的執行結果,作為入參,傳遞到回撥方法中,但是回撥方法是沒有返回值的。

示例

    @Test
    public void testCompletableThenAccept() throws ExecutionException, InterruptedException {
        long startTime = System.currentTimeMillis();
        CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {
            return "dev";

        });
        CompletableFuture<Void> cp2 =  cp1.thenAccept((a) -> {
            System.out.println("上一個任務的返回結果為: " + a);
        });
     
        cp2.get();
    }

3、 thenApply/thenApplyAsync

表示第一個任務執行完成後,執行第二個回撥方法任務,會將該任務的執行結果,作為入參,傳遞到回撥方法中,並且回撥方法是有返回值的。

示例

   @Test
    public void testCompletableThenApply() throws ExecutionException, InterruptedException {
        CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {
            return "dev";

        }).thenApply((a) -> {
            if(Objects.equals(a,"dev")){
                return "dev";
            }
            return "prod";
        });

        System.out.println("當前環境為:" + cp1.get());

        //輸出: 當前環境為:dev
    }

五、異常回撥

當CompletableFuture的任務不論是正常完成還是出現異常它都會呼叫whenComplete這回撥函式。

  • 正常完成:whenComplete返回結果和上級任務一致,異常為null;
  • 出現異常:whenComplete返回結果為null,異常為上級任務的異常;

即呼叫get()時,正常完成時就獲取到結果,出現異常時就會丟擲異常,需要你處理該異常。

下面來看看示例

1、只用whenComplete

    @Test
    public void testCompletableWhenComplete() throws ExecutionException, InterruptedException {
        CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {

            if (Math.random() < 0.5) {
                throw new RuntimeException("出錯了");
            }
            System.out.println("正常結束");
            return 0.11;

        }).whenComplete((aDouble, throwable) -> {
            if (aDouble == null) {
                System.out.println("whenComplete aDouble is null");
            } else {
                System.out.println("whenComplete aDouble is " + aDouble);
            }
            if (throwable == null) {
                System.out.println("whenComplete throwable is null");
            } else {
                System.out.println("whenComplete throwable is " + throwable.getMessage());
            }
        });
        System.out.println("最終返回的結果 = " + future.get());
    }

正常完成,沒有異常時:

正常結束
whenComplete aDouble is 0.11
whenComplete throwable is null
最終返回的結果 = 0.11

出現異常時:get()會丟擲異常

whenComplete aDouble is null
whenComplete throwable is java.lang.RuntimeException: 出錯了

java.util.concurrent.ExecutionException: java.lang.RuntimeException: 出錯了
	at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
	at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)

2、whenComplete + exceptionally示例

 @Test
    public void testWhenCompleteExceptionally() throws ExecutionException, InterruptedException {
        CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("出錯了");
            }
            System.out.println("正常結束");
            return 0.11;

        }).whenComplete((aDouble, throwable) -> {
            if (aDouble == null) {
                System.out.println("whenComplete aDouble is null");
            } else {
                System.out.println("whenComplete aDouble is " + aDouble);
            }
            if (throwable == null) {
                System.out.println("whenComplete throwable is null");
            } else {
                System.out.println("whenComplete throwable is " + throwable.getMessage());
            }
        }).exceptionally((throwable) -> {
            System.out.println("exceptionally中異常:" + throwable.getMessage());
            return 0.0;
        });

        System.out.println("最終返回的結果 = " + future.get());
    }

當出現異常時,exceptionally中會捕獲該異常,給出預設返回值0.0。

whenComplete aDouble is null
whenComplete throwable is java.lang.RuntimeException: 出錯了
exceptionally中異常:java.lang.RuntimeException: 出錯了
最終返回的結果 = 0.0

六、多工組合回撥

非同步程式設計利器:CompletableFuture

1、AND組合關係

thenCombine / thenAcceptBoth / runAfterBoth都表示:當任務一和任務二都完成再執行任務三

區別在於:

  • runAfterBoth 不會把執行結果當做方法入參,且沒有返回值
  • thenAcceptBoth: 會將兩個任務的執行結果作為方法入參,傳遞到指定方法中,且無返回值
  • thenCombine:會將兩個任務的執行結果作為方法入參,傳遞到指定方法中,且有返回值

示例

    @Test
    public void testCompletableThenCombine() throws ExecutionException, InterruptedException {
        //建立執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //開啟非同步任務1
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            System.out.println("非同步任務1,當前執行緒是:" + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("非同步任務1結束");
            return result;
        }, executorService);

        //開啟非同步任務2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("非同步任務2,當前執行緒是:" + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("非同步任務2結束");
            return result;
        }, executorService);

        //任務組合
        CompletableFuture<Integer> task3 = task.thenCombineAsync(task2, (f1, f2) -> {
            System.out.println("執行任務3,當前執行緒是:" + Thread.currentThread().getId());
            System.out.println("任務1返回值:" + f1);
            System.out.println("任務2返回值:" + f2);
            return f1 + f2;
        }, executorService);

        Integer res = task3.get();
        System.out.println("最終結果:" + res);
    }

執行結果

非同步任務1,當前執行緒是:17
非同步任務1結束
非同步任務2,當前執行緒是:18
非同步任務2結束
執行任務3,當前執行緒是:19
任務1返回值:2
任務2返回值:2
最終結果:4

2、OR組合關係

applyToEither / acceptEither / runAfterEither 都表示:兩個任務,只要有一個任務完成,就執行任務三

區別在於:

  • runAfterEither:不會把執行結果當做方法入參,且沒有返回值
  • acceptEither: 會將已經執行完成的任務,作為方法入參,傳遞到指定方法中,且無返回值
  • applyToEither:會將已經執行完成的任務,作為方法入參,傳遞到指定方法中,且有返回值

示例

  @Test
    public void testCompletableEitherAsync() {
        //建立執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //開啟非同步任務1
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            System.out.println("非同步任務1,當前執行緒是:" + Thread.currentThread().getId());

            int result = 1 + 1;
            System.out.println("非同步任務1結束");
            return result;
        }, executorService);

        //開啟非同步任務2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("非同步任務2,當前執行緒是:" + Thread.currentThread().getId());
            int result = 1 + 2;
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("非同步任務2結束");
            return result;
        }, executorService);

        //任務組合
        task.acceptEitherAsync(task2, (res) -> {
            System.out.println("執行任務3,當前執行緒是:" + Thread.currentThread().getId());
            System.out.println("上一個任務的結果為:"+res);
        }, executorService);
    }

執行結果

//通過結果可以看出,非同步任務2都沒有執行結束,任務3獲取的也是1的執行結果
非同步任務1,當前執行緒是:17
非同步任務1結束
非同步任務2,當前執行緒是:18
執行任務3,當前執行緒是:19
上一個任務的結果為:2

注意

如果把上面的核心執行緒數改為1也就是

 ExecutorService executorService = Executors.newFixedThreadPool(1);

執行結果就是下面的了,會發現根本沒有執行任務3,顯然是任務3直接被丟棄了。

非同步任務1,當前執行緒是:17
非同步任務1結束
非同步任務2,當前執行緒是:17

3、多工組合

  • allOf:等待所有任務完成
  • anyOf:只要有一個任務完成

示例

allOf:等待所有任務完成

 @Test
    public void testCompletableAallOf() throws ExecutionException, InterruptedException {
        //建立執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //開啟非同步任務1
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            System.out.println("非同步任務1,當前執行緒是:" + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("非同步任務1結束");
            return result;
        }, executorService);

        //開啟非同步任務2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("非同步任務2,當前執行緒是:" + Thread.currentThread().getId());
            int result = 1 + 2;
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("非同步任務2結束");
            return result;
        }, executorService);

        //開啟非同步任務3
        CompletableFuture<Integer> task3 = CompletableFuture.supplyAsync(() -> {
            System.out.println("非同步任務3,當前執行緒是:" + Thread.currentThread().getId());
            int result = 1 + 3;
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("非同步任務3結束");
            return result;
        }, executorService);

        //任務組合
        CompletableFuture<Void> allOf = CompletableFuture.allOf(task, task2, task3);

        //等待所有任務完成
        allOf.get();
        //獲取任務的返回結果
        System.out.println("task結果為:" + task.get());
        System.out.println("task2結果為:" + task2.get());
        System.out.println("task3結果為:" + task3.get());
    }

anyOf: 只要有一個任務完成

    @Test
    public void testCompletableAnyOf() throws ExecutionException, InterruptedException {
        //建立執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //開啟非同步任務1
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            int result = 1 + 1;
            return result;
        }, executorService);

        //開啟非同步任務2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            int result = 1 + 2;
            return result;
        }, executorService);

        //開啟非同步任務3
        CompletableFuture<Integer> task3 = CompletableFuture.supplyAsync(() -> {
            int result = 1 + 3;
            return result;
        }, executorService);

        //任務組合
        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(task, task2, task3);
        //只要有一個有任務完成
        Object o = anyOf.get();
        System.out.println("完成的任務的結果:" + o);
    }

七、CompletableFuture使用有哪些注意點

非同步程式設計利器:CompletableFuture

CompletableFuture 使我們的非同步程式設計更加便利的、程式碼更加優雅的同時,我們也要關注下它,使用的一些注意點。

1、Future需要獲取返回值,才能獲取異常資訊

  @Test
    public void testWhenCompleteExceptionally() {
        CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {
            if (1 == 1) {
                throw new RuntimeException("出錯了");
            }
            return 0.11;
        });

        //如果不加 get()方法這一行,看不到異常資訊
        //future.get();
    }

Future需要獲取返回值,才能獲取到異常資訊。如果不加 get()/join()方法,看不到異常資訊。

小夥伴們使用的時候,注意一下哈,考慮是否加try...catch...或者使用exceptionally方法。

2、CompletableFuture的get()方法是阻塞的。

CompletableFuture的get()方法是阻塞的,如果使用它來獲取非同步呼叫的返回值,需要新增超時時間。

//反例
 CompletableFuture.get();
//正例
CompletableFuture.get(5, TimeUnit.SECONDS);

3、不建議使用預設執行緒池

CompletableFuture程式碼中又使用了預設的ForkJoin執行緒池,處理的執行緒個數是電腦CPU核數-1。在大量請求過來的時候,處理邏輯複雜的話,響應會很慢。一般建議使用

自定義執行緒池,優化執行緒池配置引數。

4、自定義執行緒池時,注意飽和策略

CompletableFuture的get()方法是阻塞的,我們一般建議使用future.get(5, TimeUnit.SECONDS)。並且一般建議使用自定義執行緒池。

但是如果執行緒池拒絕策略是DiscardPolicy或者DiscardOldestPolicy,當執行緒池飽和時,會直接丟棄任務,不會拋棄異常。因此建議,CompletableFuture執行緒池策略最好

使用AbortPolicy,然後耗時的非同步執行緒,做好執行緒池隔離哈。



感謝

宣告: 公眾號如需轉載該篇文章,發表文章的頭部一定要 告知是轉至公眾號: 後端元宇宙。同時也可以問本人要markdown原稿和原圖片。其它情況一律禁止轉載!

相關文章