Java 異步編程的幾種方式
異步編程是讓程序并發運行的一種手段。它允許多個事情 同時發生
,當程序調用需要長時間運行的方法時,它不會阻塞當前的執行流程,程序可以繼續運行,當方法執行完成時通知給主線程根據需要獲取其執行結果或者失敗異常的原因。使用異步編程可以大大提高我們程序的吞吐量,可以更好的面對更高的并發場景并更好的利用現有的系統資源,同時也會一定程度上減少用戶的等待時間等。本文我們一起來看看在 Java
語言中使用異步編程有哪些方式。
Thread 方式
在 Java
語言中最簡單使用異步編程的方式就是創建一個 Thread
來實現,如果你使用的 JDK
版本是 8 以上的話,可以使用 Lambda 表達式 會更加簡潔。為了能更好的體現出異步的高效性,下面提供同步版本和異步版本的示例作為對照:
- /**
- * @author mghio
- * @since 2021-08-01
- */
- public class SyncWithAsyncDemo {
- public static void doOneThing() {
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("doOneThing ---->>> success");
- }
- public static void doOtherThing() {
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("doOtherThing ---->>> success");
- }
- public synchronized static void main(String[] args) throws InterruptedException {
- StopWatch stopWatch = new StopWatch("SyncWithAsyncDemo");
- stopWatch.start();
- // 同步調用版本
- // testSynchronize();
- // 異步調用版本
- testAsynchronize();
- stopWatch.stop();
- System.out.println(stopWatch);
- }
- private static void testAsynchronize() throws InterruptedException {
- System.out.println("-------------------- testAsynchronize --------------------");
- // 創建一個線程執行 doOneThing
- Thread doOneThingThread = new Thread(SyncWithAsyncDemo::doOneThing, "doOneThing-Thread");
- doOneThingThread.start();
- doOtherThing();
- // 等待 doOneThing 線程執行完成
- doOneThingThread.join();
- }
- private static void testSynchronize() {
- System.out.println("-------------------- testSynchronize --------------------");
- doOneThing();
- doOtherThing();
- }
- }
同步執行的運行如下:
注釋掉同步調用版本的代碼,得到異步執行的結果如下:
從兩次的運行結果可以看出,同步版本耗時 4002 ms
,異步版本執行耗時 2064 ms
,異步執行耗時減少將近一半,可以看出使用異步編程后可以大大縮短程序運行時間。
上面的示例的異步線程代碼在 main
方法內開啟了一個線程 doOneThing-Thread
用來異步執行 doOneThing
任務,在這時該線程與 main
主線程并發運行,也就是任務 doOneThing
與任務 doOtherThing
并發運行,則等主線程運行完 doOtherThing
任務后同步等待線程 doOneThing
運行完畢,整體還是比較簡單的。
但是這個示例只能作為示例使用,如果用到了生產環境發生事故后果自負,使用上面這種 Thread
方式異步編程存在兩個明顯的問題。
- FutureTask
FutureTask 方式
自 JDK 1.5
開始,引入了 Future
接口和實現 Future
接口的 FutureTask
類來表示異步計算結果。這個 FutureTask
類不僅實現了 Future
接口還實現了 Runnable
接口,表示一種可生成結果的 Runnable
。其可以處于這三種狀態:
- 未啟動 當創建一個
FutureTask
沒有執行FutureTask.run()
方法之前 - 已啟動 在
FutureTask.run()
方法執行的過程中 - 已完成 在
FutureTask.run()
方法正常執行結果或者調用了FutureTask.cancel(boolean mayInterruptIfRunning)
方法以及在調用FutureTask.run()
方法的過程中發生異常結束后
FutureTask
類實現了 Future
接口的開啟和取消任務、查詢任務是否完成、獲取計算結果方法。要獲取 FutureTask
任務的結果,我們只能通過調用 getXXX()
系列方法才能獲取,當結果還沒出來時候這些方法會被阻塞,同時這了任務可以是 Callable
類型(有返回結果),也可以是 Runnable
類型(無返回結果)。我們修改上面的示例把兩個任務方法修改為返回 String
類型,使用 FutureTask
的方法如下:
- private static void testFutureTask() throws ExecutionException, InterruptedException {
- System.out.println("-------------------- testFutureTask --------------------");
- // 創建一個 FutureTask(doOneThing 任務)
- FutureTask<String> futureTask = new FutureTask<>(FutureTaskDemo::doOneThing);
- // 使用線程池執行 doOneThing 任務
- ForkJoinPool.commonPool().execute(futureTask);
- // 執行 doOtherThing 任務
- String doOtherThingResult = doOtherThing();
- // 同步等待線程執行 doOneThing 任務結束
- String doOneThingResult = futureTask.get();
- // 任務執行結果輸出
- System.out.println("doOneThingResult ---->>> " + doOneThingResult);
- System.out.println("doOtherThingResult ---->>> " + doOtherThingResult);
- }
使用 FutureTask
異步編程方式的耗時和上面的 Thread
方式是差不多的,其本質都是另起一個線程去做 doOneThing
任務然后等待返回,運行結果如下:
這個示例中, doOneThing
和 doOtherThing
都是有返回值的任務(都返回 String
類型結果),我們在主線程 main
中創建一個異步任務 FutureTask
來執行 doOneThing
,然后使用 ForkJoinPool.commonPool()
創建線程池(有關 ForkJoinPool
的介紹見這里),然后調用了線程池的 execute
方法把 futureTask
提交到線程池來執行。
通過示例可以看到,雖然 FutureTask
提供了一些方法讓我們獲取任務的執行結果、任務是否完成等,但是使用還是比較復雜,在一些較為復雜的場景(比如多個 FutureTask
之間的關系表示)的編碼還是比較繁瑣,還是當我們調用 getXXX()
系列方法時還是會在任務執行完畢前阻塞調用線程,達不到異步編程的效果,基于這些問題,在 JDK 8
中引入了 CompletableFuture
類,下面來看看如何使用 CompletableFuture
來實現異步編程。
CompletableFuture 方式
JDK 8
中引入了 CompletableFuture
類,實現了 Future
和 CompletionStage
接口,為異步編程提供了一些列方法,如 supplyAsync
、 runAsync
和 thenApplyAsync
等,除此之外 CompletableFuture
還有一個重要的功能就是可以讓兩個或者多個 CompletableFuture
進行運算來產生結果。代碼如下:
- /**
- * @author mghio
- * @since 2021-08-01
- */
- public class CompletableFutureDemo {
- public static CompletableFuture<String> doOneThing() {
- return CompletableFuture.supplyAsync(() -> {
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return "doOneThing";
- });
- }
- public static CompletableFuture<String> doOtherThing(String parameter) {
- return CompletableFuture.supplyAsync(() -> {
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return parameter + " " + "doOtherThing";
- });
- }
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- StopWatch stopWatch = new StopWatch("CompletableFutureDemo");
- stopWatch.start();
- // 異步執行版本
- testCompletableFuture();
- stopWatch.stop();
- System.out.println(stopWatch);
- }
- private static void testCompletableFuture() throws InterruptedException, ExecutionException {
- // 先執行 doOneThing 任務,后執行 doOtherThing 任務
- CompletableFuture<String> resultFuture = doOneThing().thenCompose(CompletableFutureDemo::doOtherThing);
- // 獲取任務結果
- String doOneThingResult = resultFuture.get();
- // 獲取執行結果
- System.out.println("DoOneThing and DoOtherThing execute finished. result = " + doOneThingResult);
- }
- }
執行結果如下:
在主線程 main
中首先調用了方法 doOneThing()
方法開啟了一個異步任務,并返回了對應的 CompletableFuture
對象,我們取名為 doOneThingFuture
,然后在 doOneThingFuture
的基礎上使用 CompletableFuture
的 thenCompose()
方法,讓 doOneThingFuture
方法執行完成后,使用其執行結果作為 doOtherThing(String parameter)
方法的參數創建的異步任務返回。
我們不需要顯式使用 ExecutorService
,在 CompletableFuture
內部使用的是 Fork/Join
框架異步處理任務,因此,它使我們編寫的異步代碼更加簡潔。此外, CompletableFuture
類功能很強大其提供了和很多方便的方法,更多關于 CompletableFuture
的使用請見這篇。