一起來學Java8(九)——CompletableFuture

同步異步

計算機技術發展迅猛,不管是在軟件還是硬件方面都發展的非常快,電腦的CPU也在更新換代,強勁的CPU可以承擔更多的任務。如果程序一直使用同步編程的話,那麼將會浪費CPU資源。舉個列子,一個CPU有10個通道,如果所有程序都走一個通道,那麼剩餘9個通道都是空閒的,那這9個通道都浪費掉了。

如果使用異步編程,那麼其它9個通道都可以利用起來了,程序的吞吐量也上來了。也就是說要充分利用CPU資源,使其忙碌起來,而異步編程無疑是讓其忙碌的一種方式。

CompletableFuture

在CompletableFuture出來之前,我們可以用Future接口進行異步編程,Future配合線程池一起工作,它把任務交給線程池,線程池中處理完畢後通過Future.get()方法來獲取結果,Future.get()可以理解爲一個回調操作,在回調之前我們還可以做其他事情。

下面一個例子用來模擬小明借圖書場景:

  1. 小明去圖書館借書
  2. 圖書管理員找書(異步操作)
  3. 小明邊玩手機邊等待
  4. 小明拿到書
public class FutureTest extends TestCase {
    // 申明一個線程池
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
            5,
            0,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());

    public void testBook() {
        String bookName = "《飄》";
        System.out.println("小明去圖書館借書");
        Future<String> future = threadPoolExecutor.submit(() -> {
            // 模擬圖書管理員找書花費時間
            long minutes = (long) (Math.random() * 10) + 1;
            System.out.println("圖書管理員花費了" + minutes + "分鐘,找到了圖書" + bookName);
            Thread.sleep((long) (Math.random() * 2000));
            return bookName;
        });
        // 等待過程中做其他事情
        this.playPhone();
        try {
            String book = future.get();
            System.out.println("小明拿到了圖書" + book);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    private void playPhone() {
        System.out.println("小明在玩手機等待圖書");
    }

}

這是一個典型的Future使用方式,其中future.get()方法是阻塞的,程序運行會停留在這一行,我們的程序不可能一直等待下去,這個時候可以用future.get(long timeout, TimeUnit unit)方法,給定一個等待時間,如果超過等待時間還是沒有拿到數據則拋出一個TimeoutException異常,我們可以catch這個異常,然後對異常情況做出處理。

現在假設小明最多等待2分鐘,那麼代碼可以這麼寫:

String book = future.get(2, TimeUnit.MINUTES);

現在有這麼一種情況,假設圖書管理員找到書本之後,還需要交給助理,讓助理錄入圖書信息,錄完信息才把書交給小明。助理錄入的過程也是異步的,也就是說,我們要實現多個異步進行流水線這樣的功能。

可以發現Future用來處理多異步流水線非常困難,這個時候CompletableFuture就派上用場了,CompletableFuture自帶流水線特性,就好比Collection對應的Stream。

下面來看下CompletableFuture的基本用法,我們將上面的例子使用CompletableFuture實現:

public class CompletableFutureTest extends TestCase {

    public void testBook() {
        String bookName = "《飄》";
        System.out.println("小明去圖書館借書");
        CompletableFuture<String> future = new CompletableFuture<>();
        new Thread(() -> {
            // 模擬圖書管理員找書花費時間
            long minutes = (long) (Math.random() * 10) + 1;
            System.out.println("圖書管理員花費了" + minutes + "分鐘,找到了圖書" + bookName);
            try {
                Thread.sleep((long) (Math.random() * 2000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            future.complete(bookName);
        }).start();
        // 等待過程中做其他事情
        this.playPhone();
        try {
            String book = future.get();
            System.out.println("小明拿到了圖書" + book);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    private void playPhone() {
        System.out.println("小明在玩手機等待圖書");
    }

}

其中future.complete(bookName);的意思是將結果返回,然後調用future.get()的地方就能獲取到數據。

CompletableFuture還提供了一個靜態方法CompletableFuture.supplyAsync(Supplier)用來快速創建任務,參數Supplier用來返回任務結果。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 模擬圖書管理員找書花費時間
    long minutes = (long) (Math.random() * 10) + 1;
    System.out.println("圖書管理員花費了" + minutes + "分鐘,找到了圖書《飄》");
    try {
        Thread.sleep((long) (Math.random() * 2000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "《飄》";
});

如果不需要返回結果可以使用CompletableFuture.runAsync(Runnable)方法:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> System.out.println("running"))

接下來,我們使用CompletableFuture完成上面說到的需求:圖書管理員找到書本之後,還需要交給助理,讓助理錄入圖書信息

我們需要用到CompletableFuture.thenCompose(Function)方法,用法如下

public void testBook3() {
    String bookName = "《飄》";
    System.out.println("小明去圖書館借書");
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        // 模擬圖書管理員找書花費時間
        long minutes = (long) (Math.random() * 10) + 1;
        System.out.println("圖書管理員花費了" + minutes + "分鐘,找到了圖書"+bookName);
        try {
            Thread.sleep((long) (Math.random() * 2000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return bookName;
    })
            // thenCompose,加入第二個異步任務
            .thenCompose((book/*這裏的參數是第一個異步返回結果*/) -> CompletableFuture.supplyAsync(()-> {
        System.out.println("助理錄入圖書信息");
        return book;
    }));
    // 等待過程中做其他事情
    this.playPhone();
    try {
        String book = future.get();
        System.out.println("小明拿到了圖書" + book);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

thenApply(),thenAccept(),thenCompose(),thenCombine()

thenApply(Function)的意思是對CompletableFuture返回結果做進一步處理,然後返回一個新的結果,它的參數使用的是Function,意味着可以使用lambda表達式,表達式會提供一個參數,然後需要一個返回結果。

public void testThenApply() throws ExecutionException, InterruptedException {
    int i = 0;
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> i + 1)
    // 將相加後的結果轉成字符串
    // v就是上面i+1後的結果
    // 等同於:.thenApply((v) -> String.valueOf(v))
            .thenApply(String::valueOf);
    String str = future.get();
    System.out.println("String value: " + str);
}

如果不需要返回結果,可以用thenAccept(Consumer<? super T> action)

thenApply(),thenAccept()的區別是,thenApply提供參數,需要返回值,thenAccept只提供參數不需要返回值。

thenCompose()的用法如下:

completableFuture1.thenCompose((completableFuture1_result) -> completableFuture2)

這段代碼的意思是將completableFuture1中返回的結果帶入到completableFuture2中去執行,然後返回completableFuture2中的結果,下面是一個簡單的實例:

public void testThenCompose() throws ExecutionException, InterruptedException {
        int i=0;
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> i + 1)
                .thenCompose((j) -> CompletableFuture.supplyAsync(() -> j + 2));
        Integer result = future.get();
    }

打印:

result:3

thenCombine()方法是將兩個CompletableFuture任務結果組合起來

public void testThenCombine() throws ExecutionException, InterruptedException {
    int i=0;
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> i + 1)
            .thenCombine(CompletableFuture.supplyAsync(() -> i + 2), (result1, result2) -> {
                System.out.println("第一個CompletableFuture結果:" + result1);
                System.out.println("第二個CompletableFuture結果:" + result2);
                return result1 + result2;
            });
    Integer total = future.get();
    System.out.println("總和:" + total);
}

打印:

第一個CompletableFuture結果:1
第二個CompletableFuture結果:2
總和:3

CompletableFuture.join()

假設有一組CompletableFuture對象,現在需要這些CompletableFuture任務全部執行完畢,然後再接着做某些事情。針對這個需求,我們可以使用CompletableFuture.join()方法。

public void testJoin() {
    List<CompletableFuture> futures = new ArrayList<>();
    System.out.println("100米跑步比賽開始");
    for (int i = 0; i < 10; i++) {
        final int num = i + 1;
        futures.add(CompletableFuture.runAsync(() -> {
            int v =  (int)(Math.random() * 10);
            try {
                Thread.sleep(v);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(num + "號選手到達終點,用時:" + (10 + v) + "秒");
        }));
    }
    CompletableFuture<Double>[] futureArr = futures.toArray(new CompletableFuture[futures.size()]);
    CompletableFuture.allOf(futureArr).join();
    System.out.printf("所有選手到達終點");
}

打印:

100米跑步比賽開始
3號選手到達終點,用時:16秒
1號選手到達終點,用時:15秒
2號選手到達終點,用時:11秒
5號選手到達終點,用時:13秒
4號選手到達終點,用時:15秒
8號選手到達終點,用時:10秒
6號選手到達終點,用時:18秒
10號選手到達終點,用時:12秒
7號選手到達終點,用時:19秒
9號選手到達終點,用時:18秒
所有選手到達終點

whenComplete()

如果需要在任務處理完畢後做一些處理,可以使用whenComplete(BiConsumer)whenCompleteAsync(BiConsumer)

String bookName = "《飄》";
CompletableFuture<String> future = CompletableFuture.
        supplyAsync(() -> {System.out.println("圖書管理員開始找書");return bookName;})
        .thenApply((book) -> {System.out.println("找到書本,助理開始錄入信息"); return book;})
        .whenCompleteAsync(((book, throwable) -> {
            System.out.println("助理錄入信息完畢,通知小明來拿書");
}));
String book = future.get();
System.out.println("小明拿到書" + book);

打印:

圖書管理員開始找書
找到書本,助理開始錄入信息
助理錄入信息完畢,通知小明來拿書
小明拿到書《飄》

異常處理

對異常的處理通常分爲兩步,第一步拋出異常,第二步捕獲異常,首先我們來看下CompletableFuture如何拋出異常。

CompletableFuture拋出異常有兩種方式,第一種方式,如果CompletableFuture是直接new出來的對象,必須使用future.completeExceptionally(e)拋出異常,如果採用throw new RuntimeException(e);方式拋出異常,調用者是捕獲不到的。

CompletableFuture<String> future = new CompletableFuture<>();
new Thread(()->{
    String value = "http://";
    try {
        // 模擬出錯,給一個不存在的ENCODE
        value = URLEncoder.encode(value, "UTF-888");
        future.complete(value);
    } catch (UnsupportedEncodingException e) {
        // future處理異常
        future.completeExceptionally(e);
        // !!此方式調用者無法捕獲異常
        // throw new RuntimeException(e);
    }
}).start();

第二中方式,如果CompletableFuture對象是由工廠方法(如CompletableFuture.supplyAsync())創建的,可以直接throw new RuntimeException(e),因爲supplyAsync()封裝的方法內部做了try…catch處理

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    String value = "http://";
    try {
        // 模擬出錯,給一個不存在的ENCODE
        value = URLEncoder.encode(value, "UTF-88");
        return value;
    } catch (UnsupportedEncodingException e) {
        // 這樣不行,可以throw
        //future.completeExceptionally(e);
        throw new RuntimeException(e);
    }
});

接下來我們來看下如何捕獲異常,捕獲異常也分爲兩種。

第一種,在catch中捕獲:

try {
    future.get();
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    // 捕獲異常1,首先會到這裏來
    System.out.println("捕獲異常1,msg:" + e.getMessage());
}

第二種,在future.whenCompleteAsync()或future.whenComplete()方法中捕獲:

future.whenCompleteAsync((value, e)->{
    if (e != null) {
        // 捕獲異常2,這裏也會打印
        System.out.println("捕獲異常2, msg:" + e.getMessage());
    } else {
        System.out.println("返回結果:" + value);
    }
});

小結

CompletableFuture的出現彌補了Future接口在某些地方的不足,比如事件監聽,多任務合併,流水線操作等。同時CompletableFuture配合lambda表達式讓開發者使用起來更加方面,使得開發者在異步編程上多了一種選擇。

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