一、線程池的Future模式
在了解java8的CompletableFuture之前,先通過Future來解決一個問題,看個例子:
假設現在有一個網站,首頁有頂部Banner位、左邊欄、右邊欄、用戶信息幾大模塊需要加載,現在出一個接口,要求包裝並吐出這幾大模塊的內容
先來抽象一個首頁接口對象:
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;
}
上面的代碼會一次調用一個方法來賦值,最終返回接口對象,這個方法的最終耗時為幾個業務方法耗時的總和:
通過同步方法獲取首頁全部信息消耗時間: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,這取決於最慢的那個任務的耗時。
通過上述的例子可以發現,很多程序都是可以通過異步充分利用CPU資源的方式來進行優化處理的,單看上面的程序沒什么問題,但是仔細想想會發現太過局限,因為幾個模塊相互獨立,但在實際開發中,我們可能存在B方法需要拿到A方法的結果才可以往下進行的問題,所以上面的程序就不太適用了,java8出現了今天要說的一個內容:CompletableFuture,該類可以幫助你實現上面所說的任務順序調度,不相干的程序依然在異步,相干的存在先后順序的將會通過一定的設置來滿足自己的順序期望。
二、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的用法還有很多很多,較常用的應該就是例子里的幾種,更多的用法以后會繼續記錄到這里。