搞定 CompletableFuture,併發非同步程式設計和編寫序列程式還有什麼區別?你們要的多圖長文

日拱一兵發表於2020-07-21
  • 你有一個思想,我有一個思想,我們交換後,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo程式碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀檢視,本文同樣收錄在此,覺得不錯,還請Star?

前言

上一篇文章 不會用Java Future,我懷疑你泡茶沒我快 全面分析了 Future,通過它我們可以獲取執行緒的執行結果,它雖然解決了 Runnable 的 “三無” 短板,但是它自身還是有短板:

不能手動完成計算

假設你使用 Future 執行子執行緒呼叫遠端 API 來獲取某款產品的最新價格,伺服器由於洪災當機了,此時如果你想手動結束計算,而是想返回上次快取中的價格,這是 Future 做不到的

呼叫 get() 方法會阻塞程式

Future 不會通知你它的完成,它提供了一個get()方法,程式呼叫該方法會阻塞直到結果可用為止,沒有辦法利用回撥函式附加到Future,並在Future的結果可用時自動呼叫它

不能鏈式執行

燒水泡茶中,通過建構函式傳參做到多個任務的鏈式執行,萬一有更多的任務,或是任務鏈的執行順序有變,對原有程式的影響都是非常大的

整合多個 Future 執行結果方式笨重

假設有多個 Future 並行執行,需要在這些任務全部執行完成之後做後續操作,Future 本身是做不到的,需要藉助工具類 Executors 的方法

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
<T> T invokeAny(Collection<? extends Callable<T>> tasks)

沒有異常處理

Future 同樣沒有提供很好的異常處理方案

上一篇文章看 Future 覺得是發現了新天地,這麼一說有感覺回到瞭解放前

對於 Java 後端的同學,在 Java1.8 之前想實現非同步程式設計,還想避開上述這些煩惱,ReactiveX 應該是一個常見解決方案(做Android 的應該會有了解)。如果熟悉前端同學, ES6 Promise(男朋友的承諾)也解決了非同步程式設計的煩惱

天下語言都在彼此借鑑相應優點,Java 作為老牌勁旅自然也要解決上述問題。又是那個男人,併發大師 Doug Lea 憂天下程式設計師之憂,解天下程式設計師之困擾,在 Java1.8 版本(Lambda 橫空出世)中,新增了一個併發工具類 CompletableFuture,它的出現,讓人在泡茶過程中,品嚐到了不一樣的味道......

幾個重要 Lambda 函式

CompletableFuture 在 Java1.8 的版本中出現,自然也得搭上 Lambda 的順風車,為了更好的理解 CompletableFuture,這裡我需要先介紹一下幾個 Lambda 函式,我們只需要關注它們的以下幾點就可以:

  • 引數接受形式
  • 返回值形式
  • 函式名稱

Runnable

Runnable 我們已經說過無數次了,無引數,無返回值

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Function

Function<T, R> 接受一個引數,並且有返回值

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

Consumer

Consumer 接受一個引數,沒有返回值

@FunctionalInterface
public interface Consumer<T> {   
    void accept(T t);
}

Supplier

Supplier 沒有引數,有一個返回值

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

BiConsumer

BiConsumer<T, U> 接受兩個引數(Bi, 英文單詞詞根,代表兩個的意思),沒有返回值

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);

好了,我們做個小彙總

有些同學可能有疑問,為什麼要關注這幾個函式式介面,因為 CompletableFuture 的函式命名以及其作用都是和這幾個函式式介面高度相關的,一會你就會發現了

前戲做足,終於可以進入正題了 CompletableFuture

CompletableFuture

類結構

老規矩,先從類結構看起:

實現了 Future 介面

實現了 Future 介面,那就具有 Future 介面的相關特性,請腦補 Future 那少的可憐的 5 個方法,這裡不再贅述,具體請檢視 不會用Java Future,我懷疑你泡茶沒我快

實現了 CompletionStage 介面

CompletionStage 這個介面還是挺陌生的,中文直譯過來是【竣工階段】,如果將燒水泡茶比喻成一項大工程,他們的竣工階段體現是不一樣的

  1. 單看執行緒1 或單看執行緒 2 就是一種序列關係,做完一步之後做下一步

  2. 一起看執行緒1 和 執行緒 2,它們彼此就是並行關係,兩個執行緒做的事彼此獨立互補干擾

  3. 泡茶就是執行緒1 和 執行緒 2 的彙總/組合,也就是執行緒 1 和 執行緒 2 都完成之後才能到這個階段(當然也存線上程1 或 執行緒 2 任意一個執行緒竣工就可以開啟下一階段的場景)

所以,CompletionStage 介面的作用就做了這點事,所有函式都用於描述任務的時序關係,總結起來就是這個樣子:

CompletableFuture 既然實現了兩個介面,自然也就會實現相應的方法充分利用其介面特性,我們走進它的方法來看一看

CompletableFuture 大約有50種不同處理序列,並行,組合以及處理錯誤的方法。小弟螢幕不爭氣,方法之多,一個螢幕裝不下,看到這麼多方法,是不是瞬間要直接 收藏——>吃灰 2連走人?別擔心,我們按照相應的命名和作用進行分類,分分鐘搞定50多種方法

序列關係

then 直譯【然後】,也就是表示下一步,所以通常是一種序列關係體現, then 後面的單詞(比如 run /apply/accept)就是上面說的函式式介面中的抽象方法名稱了,它的作用和那幾個函式式介面的作用是一樣一樣滴

CompletableFuture<Void> thenRun(Runnable action)
CompletableFuture<Void> thenRunAsync(Runnable action)
CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)
  
<U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
  
CompletableFuture<Void> thenAccept(Consumer<? super T> action) 
CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)
  
<U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)  
<U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn)
<U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor)

聚合 And 關係

combine... with...both...and... 都是要求兩者都滿足,也就是 and 的關係了

<U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
<U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
<U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor)

<U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)
<U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)
<U> CompletableFuture<Void> thenAcceptBothAsync( CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action, Executor executor)
  
CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action)
CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action)
CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor)

聚合 Or 關係

Either...or... 表示兩者中的一個,自然也就是 Or 的體現了

<U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)
<U> CompletableFuture<U> applyToEitherAsync(、CompletionStage<? extends T> other, Function<? super T, U> fn)
<U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn, Executor executor)

CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor)

CompletableFuture<Void> runAfterEither(CompletionStage<?> other, Runnable action)
CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action)
CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action, Executor executor)

異常處理

CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
CompletableFuture<T> exceptionallyAsync(Function<Throwable, ? extends T> fn)
CompletableFuture<T> exceptionallyAsync(Function<Throwable, ? extends T> fn, Executor executor)
        
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)
CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor)
        
       
<U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
<U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)
<U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor)

這個異常處理看著還挺嚇人的,拿傳統的 try/catch/finally 做個對比也就瞬間秒懂了

whenComplete 和 handle 的區別如果你看接受的引數函式式介面名稱你也就能看出差別了,前者使用Comsumer, 自然也就不會有返回值;後者使用 Function,自然也就會有返回值

這裡並沒有全部列舉,不過相信很多同學已經發現了規律:

CompletableFuture 提供的所有回撥方法都有兩個非同步(Async)變體,都像這樣

// thenApply() 的變體
<U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

另外,方法的名稱也都與前戲中說的函式式介面完全匹配,按照這中規律分類之後,這 50 多個方法看起來是不是很輕鬆了呢?

基本方法已經羅列的差不多了,接下來我們通過一些例子來實際演示一下:

案例演示

建立一個 CompletableFuture 物件

建立一個 CompletableFuture 物件並沒有什麼稀奇的,依舊是通過建構函式構建

CompletableFuture<String> completableFuture = new CompletableFuture<String>();

這是最簡單的 CompletableFuture 物件建立方式,由於它實現了 Future 介面,所以自然就可以通過 get() 方法獲取結果

String result = completableFuture.get();

文章開頭已經說過,get()方法在任務結束之前將一直處在阻塞狀態,由於上面建立的 Future 沒有返回,所以在這裡呼叫 get() 將會永久性的堵塞

這時就需要我們呼叫 complete() 方法手動的結束一個 Future

completableFuture.complete("Future's Result Here Manually");

這時,所有等待這個 Future 的 client 都會返回手動結束的指定結果

runAsync

使用 runAsync 進行非同步計算

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    }
    System.out.println("執行在一個單獨的執行緒當中");
});

future.get();

由於使用的是 Runnable 函式式表示式,自然也不會獲取到結果

supplyAsync

使用 runAsync 是沒有返回結果的,我們想獲取非同步計算的返回結果需要使用 supplyAsync() 方法

		CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				throw new IllegalStateException(e);
			}
			log.info("執行在一個單獨的執行緒當中");
			return "我有返回值";
		});

		log.info(future.get());

由於使用的是 Supplier 函式式表示式,自然可以獲得返回結果

我們已經多次說過,get() 方法在Future 計算完成之前會一直處在 blocking 狀態下,對於真正的非同步處理,我們希望的是可以通過傳入回撥函式,在Future 結束時自動呼叫該回撥函式,這樣,我們就不用等待結果

CompletableFuture<String> comboText = CompletableFuture.supplyAsync(() -> {
  		//可以註釋掉做快速返回 start
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				throw new IllegalStateException(e);
			}
			log.info("?");
  		//可以註釋掉做快速返回 end
			return "贊";
		})
				.thenApply(first -> {
					log.info("在看");
					return first + ", 在看";
				})
				.thenApply(second -> second + ", 轉發");

		log.info("三連有沒有?");
		log.info(comboText.get());

對 thenApply 的呼叫並沒有阻塞程式列印log,也就是前面說的通過回撥通知機制, 這裡你看到 thenApply 使用的是supplyAsync所用的執行緒,如果將supplyAsync 做快速返回,我們再來看一下執行結果:

thenApply 此時使用的是主執行緒,所以:

序列的後續操作並不一定會和前序操作使用同一個執行緒

thenAccept

如果你不想從回撥函式中返回任何結果,那可以使用 thenAccept

		final CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(
				// 模擬遠端API呼叫,這裡只返回了一個構造的物件
				() -> Product.builder().id(12345L).name("頸椎/腰椎治療儀").build())
				.thenAccept(product -> {
					log.info("獲取到遠端API產品名稱 " + product.getName());
				});
		voidCompletableFuture.get();

thenRun

thenAccept 可以從回撥函式中獲取前序執行的結果,但thenRun 卻不可以,因為它的回撥函式式表示式定義中沒有任何引數

CompletableFuture.supplyAsync(() -> {
    //前序操作
}).thenRun(() -> {
    //序列的後需操作,無引數也無返回值
});

我們前面同樣說過了,每個提供回撥方法的函式都有兩個非同步(Async)變體,非同步就是另外起一個執行緒

		CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
			log.info("前序操作");
			return "前需操作結果";
		}).thenApplyAsync(result -> {
			log.info("後續操作");
			return "後續操作結果";
		});

到這裡,相信你序列的操作你已經非常熟練了

thenCompose

日常的任務中,通常定義的方法都會返回 CompletableFuture 型別,這樣會給後續操作留有更多的餘地,假如有這樣的業務(X唄是不是都有這樣的業務呢?):

//獲取使用者資訊詳情
	CompletableFuture<User> getUsersDetail(String userId) {
		return CompletableFuture.supplyAsync(() -> User.builder().id(12345L).name("日拱一兵").build());
	}

	//獲取使用者信用評級
	CompletableFuture<Double> getCreditRating(User user) {
		return CompletableFuture.supplyAsync(() -> CreditRating.builder().rating(7.5).build().getRating());
	}

這時,如果我們還是使用 thenApply() 方法來描述序列關係,返回的結果就會發生 CompletableFuture 的巢狀

		CompletableFuture<CompletableFuture<Double>> result = completableFutureCompose.getUsersDetail(12345L)
				.thenApply(user -> completableFutureCompose.getCreditRating(user));

顯然這不是我們想要的,如果想“拍平” 返回結果,thenCompose 方法就派上用場了

CompletableFuture<Double> result = completableFutureCompose.getUsersDetail(12345L)
				.thenCompose(user -> completableFutureCompose.getCreditRating(user));

這個和 Lambda 的map 和 flatMap 的道理是一樣一樣滴

thenCombine

如果要聚合兩個獨立 Future 的結果,那麼 thenCombine 就會派上用場了

		CompletableFuture<Double> weightFuture = CompletableFuture.supplyAsync(() -> 65.0);
		CompletableFuture<Double> heightFuture = CompletableFuture.supplyAsync(() -> 183.8);
		
		CompletableFuture<Double> combinedFuture = weightFuture
				.thenCombine(heightFuture, (weight, height) -> {
					Double heightInMeter = height/100;
					return weight/(heightInMeter*heightInMeter);
				});

		log.info("身體BMI指標 - " + combinedFuture.get());

當然這裡多數時處理兩個 Future 的關係,如果超過兩個Future,如何處理他們的一些聚合關係呢?

allOf | anyOf

相信你看到方法的簽名,你已經明白他的用處了,這裡就不再介紹了

static CompletableFuture<Void>	 allOf(CompletableFuture<?>... cfs)
static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

接下來就是異常的處理了

exceptionally

		Integer age = -1;

		CompletableFuture<String> maturityFuture = CompletableFuture.supplyAsync(() -> {
			if( age < 0 ) {
				throw new IllegalArgumentException("何方神聖?");
			}
			if(age > 18) {
				return "大家都是成年人";
			} else {
				return "未成年禁止入內";
			}
		}).thenApply((str) -> {
			log.info("遊戲開始");
			return str;
		}).exceptionally(ex -> {
			log.info("必有蹊蹺,來者" + ex.getMessage());
			return "Unknown!";
		});

		log.info(maturityFuture.get());

exceptionally 就相當於 catch,出現異常,將會跳過 thenApply 的後續操作,直接捕獲異常,進行一場處理

handle

用多執行緒,良好的習慣是使用 try/finally 正規化,handle 就可以起到 finally 的作用,對上述程式做一個小小的更改, handle 接受兩個引數,一個是正常返回值,一個是異常

注意:handle的寫法也算是正規化的一種

		Integer age = -1;

		CompletableFuture<String> maturityFuture = CompletableFuture.supplyAsync(() -> {
			if( age < 0 ) {
				throw new IllegalArgumentException("何方神聖?");
			}
			if(age > 18) {
				return "大家都是成年人";
			} else {
				return "未成年禁止入內";
			}
		}).thenApply((str) -> {
			log.info("遊戲開始");
			return str;
		}).handle((res, ex) -> {
			if(ex != null) {
				log.info("必有蹊蹺,來者" + ex.getMessage());
				return "Unknown!";
			}
			return res;
		});

		log.info(maturityFuture.get());

到這裡,關於 CompletableFuture 的基本使用你已經瞭解的差不多了,不知道你是否注意,我們前面說的帶有 Sync 的方法是單獨起一個執行緒來執行,但是我們並沒有建立執行緒,這是怎麼實現的呢?

細心的朋友如果仔細看每個變種函式的第三個方法也許會發現裡面都有一個 Executor 型別的引數,用於指定執行緒池,因為實際業務中我們是嚴謹手動建立執行緒的,這在 我會手動建立執行緒,為什麼要使用執行緒池?文章中明確說明過;如果沒有指定執行緒池,那自然就會有一個預設的執行緒池,也就是 ForkJoinPool

private static final Executor ASYNC_POOL = USE_COMMON_POOL ?
    ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

ForkJoinPool 的執行緒數預設是 CPU 的核心數。但是,在前序文章中明確說明過:

不要所有業務共用一個執行緒池,因為,一旦有任務執行一些很慢的 I/O 操作,就會導致執行緒池中所有執行緒都阻塞在 I/O 操作上,從而造成執行緒飢餓,進而影響整個系統的效能

總結

CompletableFuture 的方法並沒有全部介紹完全,也沒必要全部介紹,相信大家按照這個思路來理解 CompletableFuture 也不會有什麼大問題了,剩下的就交給實踐/時間以及自己的體會了

後記

你以為 JDK1.8 CompletableFuture 已經很完美了是不是,但追去完美的道路上永無止境,Java 9 對CompletableFuture 又做了部分升級和改造

  1. 新增了新的工廠方法

  2. 支援延遲和超時處理

    orTimeout()
    completeOnTimeout()
    
  3. 改進了對子類的支援

詳情可以檢視: Java 9 CompletableFuture API Improvements. 怎樣快速的切換不同 Java 版本來嚐鮮?SDKMAN 統一靈活管理多版本Java 這篇文章的方法送給你

最後我們們再泡一壺茶,感受一下新變化吧

靈魂追問

  1. 聽說 ForkJoinPool 執行緒池效率更高,為什麼呢?
  2. 如果批量處理非同步程式,有什麼可用的方案嗎?

參考

  1. Java 併發程式設計實戰
  2. Java 併發程式設計的藝術
  3. Java 併發程式設計之美
  4. https://www.baeldung.com/java-completablefuture
  5. https://www.callicoder.com/java-8-completablefuture-tutorial/
    個人部落格:https://dayarch.top
    加我微信好友, 進群娛樂學習交流,備註「進群」

歡迎持續關注公眾號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具彙總 | 回覆「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回覆「資料」

以讀偵探小說思維輕鬆趣味學習 Java 技術棧相關知識,本著將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......


相關文章