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();
  }

}

同步執行的運行如下:

1.png

註釋掉同步調用版本的代碼,得到異步執行的結果如下:

2.png

從兩次的運行結果可以看出,同步版本耗時 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 任務然後等待返回,運行結果如下:

3.png

這個示例中,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);
  }

}

執行結果如下:

4.png

在主線程 main 中首先調用了方法 doOneThing() 方法開啓了一個異步任務,並返回了對應的 CompletableFuture 對象,我們取名爲 doOneThingFuture,然後在 doOneThingFuture 的基礎上使用 CompletableFuture 的 thenCompose() 方法,讓 doOneThingFuture 方法執行完成後,使用其執行結果作爲 doOtherThing(String parameter) 方法的參數創建的異步任務返回。

我們不需要顯式使用 ExecutorService,在 CompletableFuture 內部使用的是 Fork/Join 框架異步處理任務,因此,它使我們編寫的異步代碼更加簡潔。此外,CompletableFuture 類功能很強大其提供了和很多方便的方法,更多關於 CompletableFuture 的使用請見 這篇

總結

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章