詳解 java CompletableFuture

背景知識

要理解 CompletableFuture,首先要弄懂什麼是 Future。因爲後者是前者的擴展。本文並不打算詳細的介紹 Future,畢竟不是本文的重點。

Future是java1.5增加的一個接口,提供了一種異步並行計算的能力。比如說主線程需要執行一個複雜耗時的計算任務,我們可以通過future把這個任務放在獨立的線程(池)中執行,然後主線程繼續處理其他任務,處理完成後再通過Future獲取計算結果。

這裏通過一個簡單的示例帶你理解下 Future。

我們有兩個服務,一個是用戶服務可以獲取用戶信息,一個是地址服務,可以通過用戶id獲取地址信息。如下,

@AllArgsConstructor
@Data
public class PersonInfo {
    private long id;
    private String name;
    private int age;
}
public class PersonService {

    public PersonInfo getPersonInfo(Long personId) throws InterruptedException {
        Thread.sleep(500);//模擬調用耗時
        //真實項目中,這裏大部分時候是通過dao從數據庫獲取
        return new PersonInfo(personId, "zhangsan", 30); //返回一條
    }
}
@AllArgsConstructor
public class AddressInfo {
    private String addressLine;
    private String city;
    private String province;
    private String country;
}
public class AddressService {

    public AddressInfo getAddress(long personId) throws InterruptedException {
        Thread.sleep(600); //模擬調用耗時
        System.out.println("id:" + personId);
        //真實項目中,這裏大部分時候是通過dao從數據庫獲取
        return new AddressInfo("勝利大街143號", "北京市", "北京", "中國");
    }
}

然後我們演示下如何在主線程使用 Future 來進行異步調用。

public class FutureTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException {

        long startTime = System.currentTimeMillis();

        PersonService personService = new PersonService();
        AddressService addressService = new AddressService();
        long personId = 100L;

        //調用用戶服務獲取用戶基本信息
        FutureTask<PersonInfo> personInfoFutureTask = new FutureTask<>(new Callable<PersonInfo>() {
            @Override
            public PersonInfo call() throws Exception {
                return personService.getPersonInfo(personId);
            }
        });
        new Thread(personInfoFutureTask).start();

        Thread.sleep(300); //模擬主線程其它操作耗時

        FutureTask<AddressInfo> addressInfoFutureTask = new FutureTask<>(new Callable<AddressInfo>() {
            @Override
            public AddressInfo call() throws Exception {
                return addressService.getAddress(personId);
            }
        });
        new Thread(addressInfoFutureTask).start();

        PersonInfo personInfo = personInfoFutureTask.get();//獲取個人信息結果
        AddressInfo addressInfo = addressInfoFutureTask.get();//獲取地址信息結果

        System.out.println("總共用時" + (System.currentTimeMillis() - startTime) + "ms");

    }
}

輸出:

總共用時909ms

很明顯,如果我們不使用 Future,而是在主線程串行的調用,耗時會是 500 + 300 + 600 = 1400 毫秒。通過 Future提供的異步計算功能,我們可以多個任務並行的執行,從而提高執行效率。

我希望你能仔細的看上面的這個示例,因爲後面講到 CompletableFuture 我會使用同一個示例。

基本介紹

通過上面的例子來看,似乎 Future 本身已經很強大了。那麼 CompletableFuture 又是做啥的呢?

雖然Future以及相關使用方法提供了異步執行任務的能力,但是對於結果的獲取卻是很不方便,只能通過阻塞或者輪詢的方式得到任務的結果。在上面的示例中,personInfoFutureTask.get() 就是阻塞調用,在線程獲取結果之前get方法會一直阻塞。

輪詢的方式在上面的示例中沒有,其實也很簡單。是Future提供了一個isDone方法,我們可以在程序中不斷的輪詢這個方法查詢執行結果。

但是,無論是阻塞方式還是輪詢方式,都不夠好。

  • 阻塞的方式和異步編程的初衷相違背
  • 輪詢的方式會耗費無謂的CPU資源

正是在這樣的背景下,CompletableFuture 在java8橫空出世。CompletableFuture提供了一種機制可以讓任務執行完成後通知監聽的一方,類似設計模式中的觀察者模式。

使用示例

首先我們來看看,上面Future那個示例,如果是用 CompletableFuture 該怎麼做?

我先給出代碼,

public class CompetableFutureTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException {

        long startTime = System.currentTimeMillis();

        PersonService personService = new PersonService();
        AddressService addressService = new AddressService();
        long personId = 100L;

        CompletableFuture<PersonInfo> personInfoCompletableFuture = CompletableFuture.supplyAsync(() -> personService.getPersonInfo(personId));
        CompletableFuture<AddressInfo> addressInfoCompletableFuture = CompletableFuture.supplyAsync(() -> addressService.getAddress(personId));

        Thread.sleep(300); //模擬主線程其它操作耗時

        PersonInfo personInfo = personInfoCompletableFuture.get();//獲取個人信息結果
        AddressInfo addressInfo = addressInfoCompletableFuture.get();//獲取地址信息結果

        System.out.println("總共用時" + (System.currentTimeMillis() - startTime) + "ms");

    }
}

首先我們實現同樣的功能代碼簡潔很多。supplyAsync 支持異步地執行我們指定的方法,這個例子中的異步執行方法是調用service。我們也可以使用 Executor 執行異步程序,默認是 ForkJoinPool.commonPool()。

另外通過這個示例,可以發現我們完全可以使用 CompletableFuture 代替 Future。

當然 CompletableFuture 的功能遠不止與此,不然它的存在就沒有意義了。CompletableFuture 提供了幾十種方法輔助我們操作異步任務,用好了這些方法可以寫出更加簡潔,高效的代碼。比如下面這個例子:

CompletableFuture<PersonInfo> personInfoCompletableFuture = CompletableFuture.supplyAsync(() -> personService.getPersonInfo(personId));
        CompletableFuture<AddressInfo> addressInfoCompletableFuture = CompletableFuture.supplyAsync(() -> addressService.getAddress(personId));


        final CompletableFuture<Void> completableFutureAllOf =
                CompletableFuture.allOf(personInfoCompletableFuture, addressInfoCompletableFuture);

        completableFutureAllOf.get(); //執行時間以最長那個任務爲準

        PersonInfo personInfo = personInfoCompletableFuture.get();//馬上返回
        AddressInfo addressInfo = addressInfoCompletableFuture.get();//馬上返回

這個示例中,allOf可以讓我們把多個異步任務結果的獲取整合起來,這樣操作更簡單,代碼更簡潔。

前面提到了它可以解決的痛點,就是提供了一種類似觀察者模式的機制,當異步的計算結果完成後可以通知監聽者。下面來看個示例,

CompletableFuture<PersonInfo> personInfoCompletableFuture = CompletableFuture.supplyAsync(() -> personService.getPersonInfo(personId));
        CompletableFuture<AddressInfo> addressInfoCompletableFuture = CompletableFuture.supplyAsync(() -> addressService.getAddress(personId));


        final CompletableFuture<Void> completableFutureAllOf =
                CompletableFuture.allOf(personInfoCompletableFuture, addressInfoCompletableFuture);

        //監聽執行結果,整合兩個任務的結果進一步處理
        final CompletableFuture<PersonAndAddress> personAndAddressCompletableFuture = completableFutureAllOf.thenApply((voidInput) ->
                new PersonAndAddress(personInfoCompletableFuture.join(), addressInfoCompletableFuture.join()));

        personAndAddressCompletableFuture.join();//以時間長的任務爲準

在上面這個示例中,當兩個異步任務執行完畢後,我們可以通過thenApply監聽到結果並進行處理。

CompletableFuture 還有很多好玩有用的功能,如果感興趣可以自行研究下。

總結

通過前面的講解,你應該對 Future 以及它的擴展接口 CompletableFuture 都有了比較深入的認識。

我個人的建議是如果你的項目是基於java8,大部分情況你應該用後者而不是前者。如果你的項目是java8之前的版本,也建議你使用第三方的工具比如 Guava 等框架提供的Future工具類。

參考:

  • https://www.ibm.com/developerworks/cn/java/j-cf-of-jdk8/index.html

關注公衆號:犀牛飼養員的技術筆記

個人博客:http://www.machengyu.net

csdn博客: https://blog.csdn.net/pony_maggie

思否: https://segmentfault.com/u/machengyu/articles

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