利用CompletableFuture優化程序的執行效率

1、線程池的Future模式

在瞭解java8的CompletableFuture以前,先經過Future來解決一個問題,看個例子:java

假設如今有一個網站,首頁有頂部Banner位、左邊欄、右邊欄、用戶信息幾大模塊須要加載,如今出一個接口,要求包裝並吐出這幾大模塊的內容web

先來抽象一個首頁接口對象:shell

public class WebModule {

    private String top; //頂部Banner位

    private String left; //左邊欄

    private String right; //右邊欄

    private String user; //用戶信息

    //...get...set...

    @Override
    public String toString() {
        return String.format("top: %s;  left: %s;  right: %s;  user: %s", top, left, right, user);
    }
}

如今提供下面幾個業務方法來獲取這些信息:多線程

private String getTop() { // 這裏假設getTop須要執行200ms
        try {
            Thread.sleep(200L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "頂部banner位";
    }

    private String getLeft() { // 這裏假設getLeft須要執行50ms
        try {
            Thread.sleep(50L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "左邊欄";
    }

    private String getRight() { // 這裏假設getRight須要執行80ms
        try {
            Thread.sleep(80L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "右邊欄";
    }

    private String getUser() { // 這裏假設getUser須要執行100ms
        try {
            Thread.sleep(100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "用戶信息";
    }

ok,如今來實現下這個接口:異步

// 同步獲取
public WebModule getWebModuleMsgSync() {
   WebModule webModule = new WebModule();
   webModule.setTop(getTop());
   webModule.setLeft(getLeft());
   webModule.setRight(getRight());
   webModule.setUser(getUser());
   return webModule;
}

上面的代碼會一次調用一個方法來賦值,最終返回接口對象,這個方法的最終耗時爲幾個業務方法耗時的總和:ide

經過同步方法獲取首頁所有信息消耗時間:435ms
結果爲:top: 頂部banner位;  left: 左邊欄;  right: 右邊欄;  user: 用戶信息

430ms左右的執行時間,其實這幾個模塊是相互獨立沒有影響的,所以可使用線程池的Future模式來進行多線程處理優化:優化

// 異步獲取
public WebModule getWebModuleMsgAsync() throws ExecutionException, InterruptedException {
   Future top = executorService.submit(this::getTop);
   Future left = executorService.submit(this::getLeft);
   Future right = executorService.submit(this::getRight);
   Future user = executorService.submit(this::getUser);
   WebModule webModule = new WebModule();
   webModule.setTop(top.get());
   webModule.setLeft(left.get());
   webModule.setRight(right.get());
   webModule.setUser(user.get());
   return webModule;
}

這幾個方法會被異步執行,get方法會被阻塞,直到執行結束,運行結果以下:網站

經過異步方法獲取首頁所有信息消耗時間:276ms
結果爲:top: 頂部banner位;  left: 左邊欄;  right: 右邊欄;  user: 用戶信息

能夠看到,執行速度幾乎降了近200ms,這取決於最慢的那個任務的耗時。this

 

經過上述的例子能夠發現,不少程序都是能夠經過異步充分利用CPU資源的方式來進行優化處理的,單看上面的程序沒什麼問題,可是仔細想一想會發現太過侷限,由於幾個模塊相互獨立,但在實際開發中,咱們可能存在B方法須要拿到A方法的結果才能夠往下進行的問題,因此上面的程序就不太適用了,java8出現了今天要說的一個內容:CompletableFuture,該類能夠幫助你實現上面所說的任務順序調度,不相干的程序依然在異步,相干的存在前後順序的將會經過必定的設置來知足本身的順序指望。線程

2、CompletableFuture

如今再來假設一個例子,如今存在如下幾個方法的調用:

zero方法、a方法、b方法、ab方法、c方法、d方法、e方法

定義以下:

//各個方法,sleep當成是執行時間
    
    private void zero() {
        sleep(100L);
        System.out.println("zero方法觸發!\n-----------------------------");
    }

    private String a() {
        sleep(500L);
        return "a";
    }

    private String b(String a) {
        sleep(1000L);
        return a + "b";
    }

    private String c() {
        sleep(500L);
        return "c";
    }

    private String ab(String a, String b) {
        sleep(100L);
        return a + "|" + b;
    }

    private void d(String a) {
        sleep(1000L);
        System.out.println("d方法觸發,拿到的a = " + a);
    }

    private String e(String a) {
        sleep(100L);
        return a + "e";
    }

根據上面的方法定義,能夠整理出來其執行關係:

zero、a、c都是獨立調用的方法,而b、d、e方法都須要拿到a的執行結果值才能觸發,ab方法則要求更加苛刻,須要同時拿到a和b的執行結果才能夠觸發,如今假設須要把全部的方法都觸發一遍,咱們又指望經過異步的方式來儘量的優化代碼,這個時候若是還用上面例子裏的方式,恐怕就很難進行下去了,由於不少方法存在相互依賴的現象,不過如今有了CompletableFuture,這個問題就能夠解決了,來看下代碼(方法及做用都寫在註釋上了,下面的文章就很少作說明了):

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

        long s = System.currentTimeMillis();
        Test t = new Test();

        //runAsync用於執行沒有返回值的異步任務
        CompletableFuture future0 = CompletableFuture.runAsync(t::zero)
                .exceptionally(e -> {
                    System.out.println("Zero出錯!");
                    return null;
                }); //這裏是異常處理,指的是該異步任務執行中出錯,應該作的處理

        //supplyAsync方法用於執行帶有返回值的異步任務
        CompletableFuture futureA = CompletableFuture.supplyAsync(t::a)
                .exceptionally(e -> {
                    System.out.println("方法A出錯!");
                    return null;
                });

        //thenCompose方法用於鏈接兩個CompletableFuture任務,以下表明futureA結束後將執行結果交由另一個CompletableFuture處理,而後將執行鏈路最終賦值給futureB
        CompletableFuture futureB = futureA.thenCompose(a -> CompletableFuture.supplyAsync(() -> t.b(a)))
                .exceptionally(e -> {
                    System.out.println("方法B出錯!");
                    return null;
                });

        //thenAccept方法用於將一個任務的結果,傳給須要該結果的任務,以下表示futureD的執行須要futureA的結果,與thenApply不一樣的是,這個方法沒有有返回值
        CompletableFuture futureD = futureA.thenAccept(t::d);

        //thenApply方法用於將一個任務的結果,傳給須要該結果的任務,以下表示futureE的執行須要futureA的結果,與thenAccept不一樣的是,這個方法有返回值
        CompletableFuture futureE = futureA.thenApply(t::e)
                .exceptionally(e -> {
                    System.out.println("方法E出錯!");
                    return null;
                });

        /**
         * thenApply方法概念容易與thenCompose混淆,畢竟最終目的很類似
         */

        //thenCombine方法用於鏈接多個異步任務的結果,以下ab方法須要futureA和futureB的執行結果,那麼就可使用thenCombine進行鏈接
        //注意,執行到ab這裏,說明futureA和futureB必定已經執行完了
        CompletableFuture futureAB = futureA.thenCombine(futureB, t::ab)
                .exceptionally(e -> {
                    System.out.println("方法AB出錯!");
                    return null;
                });

        //單純的一個異步任務,不依賴任何其餘任務
        CompletableFuture futureC = CompletableFuture.supplyAsync(t::c)
                .exceptionally(e -> {
                    System.out.println("方法C出錯!");
                    return null;
                });

        //allOf若是阻塞結束則表示全部任務都執行結束了
        CompletableFuture.allOf(future0, futureA, futureB, futureAB, futureC, futureD, futureE).get();

        System.out.println("方法Zero輸出:" + future0.get());
        System.out.println("方法A輸出:" + futureA.get());
        System.out.println("方法B輸出:" + futureB.get());
        System.out.println("方法AB輸出:" + futureAB.get());
        System.out.println("方法C輸出:" + futureC.get());
        System.out.println("方法D輸出:" + futureD.get());
        System.out.println("方法E輸出:" + futureE.get());
        System.out.println("耗時:" + (System.currentTimeMillis() - s) + "ms");
    }

輸出結果以下:

zero方法觸發!
-----------------------------
d方法觸發,拿到的a = a
方法Zero輸出:null
方法A輸出:a
方法B輸出:ab
方法AB輸出:a|ab
方法C輸出:c
方法D輸出:null
方法E輸出:ae
耗時:1668ms

能夠看到,邏輯方面是沒有任何問題的,也按照預期的順序和方式進行了,注意看這裏的運行時間,約等於1600ms,與第一個例子時長取決於執行時間最長的那個方法不一樣,上面的例子時長取決於有序的執行鏈的耗時最長的執行時間,分析下上面的程序,順序鏈最長的,就是ab這條,ab須要a和b所有執行完,而b又依賴a的結果,所以ab執行完的時間就是500+1000的時間(a須要500ms,b又須要等待a,500ms後b觸發,b自身又須要1000ms,等都結束了,再觸發ab方法,而ab方法又須要100ms的執行時間,所以ab是最長的耗時方法,ab耗時=500+1000+100)

須要說明的是上述例子裏用到的方法,幾乎每一個都有個重載方法,用來傳遞一個線程池對象,例子裏用的都是不傳的,用的是其內部的ForkJoinPool.commonPool()。

CompletableFuture的用法還有不少不少,較經常使用的應該就是例子裏的幾種,更多的用法之後會繼續記錄到這裏。

相關文章
相關標籤/搜索