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