前言

非同步程式設計是讓程式併發執行的一種手段。它允許多個事情同時發生,當程式呼叫需要長時間執行的方法時,它不會阻塞當前的執行流程,程式可以繼續執行,當方法執行完成時通知給主執行緒根據需要獲取其執行結果或者失敗異常的原因。使用非同步程式設計可以大大提高我們程式的吞吐量,可以更好的面對更高的併發場景並更好的利用現有的系統資源,同時也會一定程度上減少使用者的等待時間等。本文我們一起來看看在 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 方式非同步程式設計存在兩個明顯的問題。

  1. 建立執行緒沒有複用。我們知道頻繁的執行緒建立與銷燬是需要一部分開銷的,而且示例裡也沒有限制執行緒的個數,如果使用不當可能會把系統執行緒用盡,從而引發事故,這個問題使用執行緒池可以解決。
  2. 非同步任務無法獲取最終的執行結果。示例中的這種方式是滿足不了的,這時候就需要使用下面介紹的第二種 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 的使用請見 這篇

總結

本文介紹了在 Java 中的 JDK 使用非同步程式設計的三種方式,這些是我們最基礎的實現非同步程式設計的工具,在其之上的還有 Guava 庫提供的 ListenableFutureFutures 類以及 Spring 框架提供的非同步執行能力,使用 @Async 等註解實現非同步處理,感興趣的話可以自行學習瞭解。