【Java并发编程实战】(十七):Future和CompletableFuture的原理及实战——异步编程没有那么难
文章目錄
- 引言
- 生活中的例子
- 場景1
- 場景2
- Java中的Future
- 如何獲取Future
- Future的主要方法及使用
- Future的核心源碼
- Future模式的高階版本—— CompletableFuture
- 如何獲取CompletableFuture
- CompletableFuture的主要方法及使用
- 小結(jié)
引言
在高性能編程中,并發(fā)編程已經(jīng)成為了極為重要的一部分。并發(fā)編程可以總結(jié)為三個(gè)核心問題:分工、同步和互斥。編寫并發(fā)程序,首先要做的就是分工,所謂分工指的是如何高效地拆解任務(wù)并分配給線程。由于并發(fā)編程比串行編程更困難,也更容易出錯(cuò),因此,我們就更需要借鑒一些前人優(yōu)秀的,成熟的設(shè)計(jì)模式,使得我們的設(shè)計(jì)更加健壯,更加完美。
而Future模式,正是其中使用最為廣泛,也是極為重要的一種設(shè)計(jì)模式。今天就跟少俠了解一手Future模式!
生活中的例子
場景1
小張喜歡沒事泡泡茶,每次都是洗水壺–>洗茶壺–>洗茶杯–>燒開水–>拿茶葉–>泡茶,如下圖,喝到茶大概得花上20分鐘。
場景2
但是小王不這么干,對于燒水泡茶這個(gè)程序,她采取的方案是下圖所示的這樣:用兩個(gè)線程T1和T2來完成燒水泡茶程序,T1負(fù)責(zé)洗水壺、燒開水、泡茶這三道工序,T2負(fù)責(zé)洗茶壺、洗茶杯、拿茶葉三道工序,其中T1在執(zhí)行泡茶這道工序時(shí)需要等待T2完成拿茶葉的工序。對于T1的這個(gè)等待動作,你應(yīng)該可以想出很多種辦法,例如Thread.join()、CountDownLatch,甚至阻塞隊(duì)列都可以解決,不過今天我們用Future特性來實(shí)現(xiàn)。
Java中的Future
如何獲取Future
// 提交Runnable任務(wù) Future submit(Runnable task);這個(gè)方法的參數(shù)是一個(gè)Runnable接口,Runnable接口的run()方法是沒有返回值的,所以 submit(Runnable task) 這個(gè)方法返回的Future僅可以用來斷言任務(wù)已經(jīng)結(jié)束了,類似于Thread.join()。
// 提交Callable任務(wù)Future submit(Callable task);這個(gè)方法的參數(shù)是一個(gè)Callable接口,它只有一個(gè)call()方法,并且這個(gè)方法是有返回值的,所以這個(gè)方法返回的Future對象可以通過調(diào)用其get()方法來獲取任務(wù)的執(zhí)行結(jié)果。
// 提交Runnable任務(wù)及結(jié)果引用 Future submit(Runnable task, T result);這個(gè)方法很有意思,假設(shè)這個(gè)方法返回的Future對象是future,future.get()的返回值就是傳給submit()方法的參數(shù)result。這個(gè)方法該怎么用呢?下面這段示例代碼展示了它的經(jīng)典用法。需要你注意的是Runnable接口的實(shí)現(xiàn)類Task聲明了一個(gè)有參構(gòu)造函數(shù) Task(Result r) ,創(chuàng)建Task對象的時(shí)候傳入了result對象,這樣就能在類Task的run()方法中對result進(jìn)行各種操作了。result相當(dāng)于主線程和子線程之間的橋梁,通過它主子線程可以共享數(shù)據(jù)。
Future的主要方法及使用
獲取到Future之后,我們怎么來進(jìn)行使用呢,Java中提供了如下幾個(gè)核心方法:
// 取消任務(wù)boolean cancel(boolean mayInterruptIfRunning);// 判斷任務(wù)是否已取消 boolean isCancelled();// 判斷任務(wù)是否已結(jié)束boolean isDone();// 獲得任務(wù)執(zhí)行結(jié)果get();// 獲得任務(wù)執(zhí)行結(jié)果,支持超時(shí)get(long timeout, TimeUnit unit);那么如何靈活的使用這幾個(gè)方法呢?下面的示例代碼就是用這一節(jié)提到的Future特性來實(shí)現(xiàn)的。首先,我們創(chuàng)建了兩個(gè)Future——task1和task2,task1完成洗水壺、燒開水、泡茶的任務(wù),task2完成洗茶壺、洗茶杯、拿茶葉的任務(wù);這里需要注意的是task1這個(gè)任務(wù)在執(zhí)行泡茶任務(wù)前,需要等待task2把茶葉拿來,所以task1內(nèi)部需要引用task2,并在執(zhí)行泡茶之前,調(diào)用task2的get()方法實(shí)現(xiàn)等待。
一次執(zhí)行結(jié)果:
Future的核心源碼
那么Future又是如何實(shí)現(xiàn)異步操作的呢,我們結(jié)合源碼來看一下。
由于Future是接口,這里我們主要看它的實(shí)現(xiàn)類FutureTask的實(shí)現(xiàn)。關(guān)鍵的部分在下面,FutureTask作為一個(gè)線程單獨(dú)執(zhí)行時(shí),會將結(jié)果保存到Object類型的變量outcome中,并設(shè)置任務(wù)的狀態(tài),下面是FutureTask的run()方法:
從FutureTask中獲得結(jié)果的實(shí)現(xiàn)如下:
Future模式的高階版本—— CompletableFuture
Future模式雖然好用,但也有一個(gè)問題,那就是將任務(wù)提交給線程后,調(diào)用線程并不知道這個(gè)任務(wù)什么時(shí)候執(zhí)行完,如果執(zhí)行調(diào)用get()方法或者isDone()方法判斷,可能會進(jìn)行不必要的等待,那么系統(tǒng)的吞吐量很難提高。
為了解決這個(gè)問題,JDK對Future模式又進(jìn)行了加強(qiáng),創(chuàng)建了一個(gè)CompletableFuture,它可以理解為Future模式的升級版本,它最大的作用是提供了一個(gè)回調(diào)機(jī)制,可以在任務(wù)完成后,自動回調(diào)一些后續(xù)的處理,這樣,整個(gè)程序可以把“結(jié)果等待”完全給移除了。
如何獲取CompletableFuture
public static CompletableFuture<Void> runAsync(Runnable runnable) public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)四個(gè)靜態(tài)方法用來為一段異步執(zhí)行的代碼創(chuàng)建CompletableFuture對象,方法的參數(shù)類型都是函數(shù)式接口,所以可以使用lambda表達(dá)式實(shí)現(xiàn)異步任務(wù)
-
runAsync方法:它以Runnable函數(shù)式接口類型為參數(shù),所以CompletableFuture的計(jì)算結(jié)果為空。
-
supplyAsync方法以Supplier函數(shù)式接口類型為參數(shù),CompletableFuture的計(jì)算結(jié)果類型為U。
說明:Async結(jié)尾的方法都是可以異步執(zhí)行的,如果指定了線程池,會在指定的線程池中執(zhí)行,如果沒有指定,默認(rèn)會在ForkJoinPool.commonPool()中執(zhí)行。
CompletableFuture的主要方法及使用
關(guān)于CompletableFuture,Java中提供了如下幾個(gè)核心方法:
1 變換結(jié)果
由于回調(diào)風(fēng)格的實(shí)現(xiàn),我們不必因?yàn)榈却粋€(gè)計(jì)算完成而阻塞著調(diào)用線程,而是告訴CompletableFuture當(dāng)計(jì)算完成的時(shí)候請執(zhí)行某個(gè)Function。還可以串聯(lián)起來。
這些方法的輸入是上一個(gè)階段計(jì)算后的結(jié)果,返回值是經(jīng)過轉(zhuǎn)化后結(jié)果:
2 消費(fèi)結(jié)果
這些方法只是針對結(jié)果進(jìn)行消費(fèi),入?yún)⑹荂onsumer,沒有返回值:
3 計(jì)算結(jié)果完成時(shí)的處理
當(dāng)CompletableFuture的計(jì)算結(jié)果完成,或者拋出異常的時(shí)候,可以執(zhí)行特定的Action。主要是下面的方法:
4 結(jié)合兩個(gè)CompletionStage的結(jié)果,進(jìn)行轉(zhuǎn)化后返回
需要上一階段的返回值,并且other代表的CompletionStage也要返回值之后,把這兩個(gè)返回值,進(jìn)行轉(zhuǎn)換后返回指定類型的值。
為了和大家一起體會CompletableFuture異步編程的優(yōu)勢,這里我們用CompletableFuture重新實(shí)現(xiàn)前面曾提及的燒水泡茶程序。首先還是需要先完成分工方案,在下面的程序中,我們分了3個(gè)任務(wù):任務(wù)1負(fù)責(zé)洗水壺、燒開水,任務(wù)2負(fù)責(zé)洗茶壺、洗茶杯和拿茶葉,任務(wù)3負(fù)責(zé)泡茶。其中任務(wù)3要等待任務(wù)1和任務(wù)2都完成后才能開始。這個(gè)分工如下圖所示。
下面是具體代碼實(shí)現(xiàn),你先略過runAsync()、supplyAsync()、thenCombine()這些不太熟悉的方法,從大局上看,你會發(fā)現(xiàn):
- 無需手工維護(hù)線程,沒有繁瑣的手工維護(hù)線程的工作,給任務(wù)分配線程的工作也不需要我們關(guān)注;
- 語義更清晰,例如 f3 = f1.thenCombine(f2, ()->{}) 能夠清晰地表述“任務(wù)3要等待任務(wù)1和任務(wù)2都完成后才能開始”;
- 代碼更簡練并且專注于業(yè)務(wù)邏輯,幾乎所有代碼都是業(yè)務(wù)邏輯相關(guān)的。
小結(jié)
今天我們主要介紹Future模式,我們從一個(gè)最簡單的Future模式開始,逐步深入,先后介紹了JDK內(nèi)部的Future模式實(shí)現(xiàn),以及對Future模式的進(jìn)化版本CompletableFuture做了簡單的介紹。對
于多線程開發(fā)而言,Future模式的應(yīng)用極其廣泛,可以說這個(gè)模式已經(jīng)成為了異步開發(fā)的基礎(chǔ)設(shè)施。
總結(jié)
以上是生活随笔為你收集整理的【Java并发编程实战】(十七):Future和CompletableFuture的原理及实战——异步编程没有那么难的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 鸿蒙系统能玩魔兽世界吗,魔兽世界:永久6
- 下一篇: 基于OpenPose的坐姿识别