同步異步
計算機技術發展迅猛,不管是在軟件還是硬件方面都發展的非常快,電腦的CPU也在更新換代,強勁的CPU可以承擔更多的任務。如果程序一直使用同步編程的話,那麼將會浪費CPU資源。舉個列子,一個CPU有10個通道,如果所有程序都走一個通道,那麼剩餘9個通道都是空閒的,那這9個通道都浪費掉了。
如果使用異步編程,那麼其它9個通道都可以利用起來了,程序的吞吐量也上來了。也就是說要充分利用CPU資源,使其忙碌起來,而異步編程無疑是讓其忙碌的一種方式。
CompletableFuture
在CompletableFuture出來之前,我們可以用Future接口進行異步編程,Future配合線程池一起工作,它把任務交給線程池,線程池中處理完畢後通過Future.get()方法來獲取結果,Future.get()可以理解爲一個回調操作,在回調之前我們還可以做其他事情。
下面一個例子用來模擬小明借圖書場景:
- 小明去圖書館借書
- 圖書管理員找書(異步操作)
- 小明邊玩手機邊等待
- 小明拿到書
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表達式讓開發者使用起來更加方面,使得開發者在異步編程上多了一種選擇。