在瞭解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,該類能夠幫助你實現上面所說的任務順序調度,不相干的程序依然在異步,相干的存在前後順序的將會經過必定的設置來知足本身的順序指望。線程
如今再來假設一個例子,如今存在如下幾個方法的調用:
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的用法還有不少不少,較經常使用的應該就是例子裏的幾種,更多的用法之後會繼續記錄到這裏。