Java 異步編程的幾種方式

前言

異步編程是讓程序并發(fā)運(yùn)行的一種手段。它允許多個(gè)事情同時(shí)發(fā)生,當(dāng)程序調(diào)用需要長(zhǎng)時(shí)間運(yùn)行的方法時(shí),它不會(huì)阻塞當(dāng)前的執(zhí)行流程,程序可以繼續(xù)運(yùn)行,當(dāng)方法執(zhí)行完成時(shí)通知給主線程根據(jù)需要獲取其執(zhí)行結(jié)果或者失敗異常的原因。使用異步編程可以大大提高我們程序的吞吐量,可以更好的面對(duì)更高的并發(fā)場(chǎng)景并更好的利用現(xiàn)有的系統(tǒng)資源,同時(shí)也會(huì)一定程度上減少用戶的等待時(shí)間等。本文我們一起來(lái)看看在 Java 語(yǔ)言中使用異步編程有哪些方式。

Thread 方式

Java 語(yǔ)言中最簡(jiǎn)單使用異步編程的方式就是創(chuàng)建一個(gè) Thread 來(lái)實(shí)現(xiàn),如果你使用的 JDK 版本是 8 以上的話,可以使用 Lambda 表達(dá)式 會(huì)更加簡(jiǎn)潔。為了能更好的體現(xiàn)出異步的高效性,下面提供同步版本和異步版本的示例作為對(duì)照:

/**
 * @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();

    // 同步調(diào)用版本
    // testSynchronize();

    // 異步調(diào)用版本
    testAsynchronize();

    stopWatch.stop();
    System.out.println(stopWatch);
  }

  private static void testAsynchronize() throws InterruptedException {
    System.out.println("-------------------- testAsynchronize --------------------");

    // 創(chuàng)建一個(gè)線程執(zhí)行 doOneThing
    Thread doOneThingThread = new Thread(SyncWithAsyncDemo::doOneThing, "doOneThing-Thread");
    doOneThingThread.start();

    doOtherThing();
    // 等待 doOneThing 線程執(zhí)行完成
    doOneThingThread.join();
  }

  private static void testSynchronize() {
    System.out.println("-------------------- testSynchronize --------------------");

    doOneThing();
    doOtherThing();
  }

}

同步執(zhí)行的運(yùn)行如下:

1.png

注釋掉同步調(diào)用版本的代碼,得到異步執(zhí)行的結(jié)果如下:

2.png

從兩次的運(yùn)行結(jié)果可以看出,同步版本耗時(shí) 4002 ms,異步版本執(zhí)行耗時(shí) 2064 ms,異步執(zhí)行耗時(shí)減少將近一半,可以看出使用異步編程后可以大大縮短程序運(yùn)行時(shí)間。

上面的示例的異步線程代碼在 main 方法內(nèi)開(kāi)啟了一個(gè)線程 doOneThing-Thread 用來(lái)異步執(zhí)行 doOneThing 任務(wù),在這時(shí)該線程與 main 主線程并發(fā)運(yùn)行,也就是任務(wù) doOneThing 與任務(wù) doOtherThing 并發(fā)運(yùn)行,則等主線程運(yùn)行完 doOtherThing 任務(wù)后同步等待線程 doOneThing 運(yùn)行完畢,整體還是比較簡(jiǎn)單的。

但是這個(gè)示例只能作為示例使用,如果用到了生產(chǎn)環(huán)境發(fā)生事故后果自負(fù),使用上面這種 Thread 方式異步編程存在兩個(gè)明顯的問(wèn)題。

  1. 創(chuàng)建線程沒(méi)有復(fù)用。我們知道頻繁的線程創(chuàng)建與銷(xiāo)毀是需要一部分開(kāi)銷(xiāo)的,而且示例里也沒(méi)有限制線程的個(gè)數(shù),如果使用不當(dāng)可能會(huì)把系統(tǒng)線程用盡,從而引發(fā)事故,這個(gè)問(wèn)題使用線程池可以解決。
  2. 異步任務(wù)無(wú)法獲取最終的執(zhí)行結(jié)果。示例中的這種方式是滿足不了的,這時(shí)候就需要使用下面介紹的第二種 FutureTask 的方式了。

FutureTask 方式

JDK 1.5 開(kāi)始,引入了 Future 接口和實(shí)現(xiàn) Future 接口的 FutureTask 類(lèi)來(lái)表示異步計(jì)算結(jié)果。這個(gè) FutureTask 類(lèi)不僅實(shí)現(xiàn)了 Future 接口還實(shí)現(xiàn)了 Runnable 接口,表示一種可生成結(jié)果的 Runnable。其可以處于這三種狀態(tài):

  • 未啟動(dòng) 當(dāng)創(chuàng)建一個(gè) FutureTask 沒(méi)有執(zhí)行 FutureTask.run() 方法之前
  • 已啟動(dòng)FutureTask.run() 方法執(zhí)行的過(guò)程中
  • 已完成FutureTask.run() 方法正常執(zhí)行結(jié)果或者調(diào)用了 FutureTask.cancel(boolean mayInterruptIfRunning) 方法以及在調(diào)用 FutureTask.run() 方法的過(guò)程中發(fā)生異常結(jié)束后

FutureTask 類(lèi)實(shí)現(xiàn)了 Future 接口的開(kāi)啟和取消任務(wù)、查詢(xún)?nèi)蝿?wù)是否完成、獲取計(jì)算結(jié)果方法。要獲取 FutureTask 任務(wù)的結(jié)果,我們只能通過(guò)調(diào)用 getXXX() 系列方法才能獲取,當(dāng)結(jié)果還沒(méi)出來(lái)時(shí)候這些方法會(huì)被阻塞,同時(shí)這了任務(wù)可以是 Callable 類(lèi)型(有返回結(jié)果),也可以是 Runnable 類(lèi)型(無(wú)返回結(jié)果)。我們修改上面的示例把兩個(gè)任務(wù)方法修改為返回 String 類(lèi)型,使用 FutureTask 的方法如下:

private static void testFutureTask() throws ExecutionException, InterruptedException {
    System.out.println("-------------------- testFutureTask --------------------");

    // 創(chuàng)建一個(gè) FutureTask(doOneThing 任務(wù))
    FutureTask<String> futureTask = new FutureTask<>(FutureTaskDemo::doOneThing);
    // 使用線程池執(zhí)行 doOneThing 任務(wù)
    ForkJoinPool.commonPool().execute(futureTask);

    // 執(zhí)行 doOtherThing 任務(wù)
    String doOtherThingResult = doOtherThing();

    // 同步等待線程執(zhí)行 doOneThing 任務(wù)結(jié)束
    String doOneThingResult = futureTask.get();

    // 任務(wù)執(zhí)行結(jié)果輸出
    System.out.println("doOneThingResult ---->>> " + doOneThingResult);
    System.out.println("doOtherThingResult ---->>> " + doOtherThingResult);
}

使用 FutureTask 異步編程方式的耗時(shí)和上面的 Thread 方式是差不多的,其本質(zhì)都是另起一個(gè)線程去做 doOneThing 任務(wù)然后等待返回,運(yùn)行結(jié)果如下:

3.png

這個(gè)示例中,doOneThingdoOtherThing 都是有返回值的任務(wù)(都返回 String 類(lèi)型結(jié)果),我們?cè)谥骶€程 main 中創(chuàng)建一個(gè)異步任務(wù) FutureTask 來(lái)執(zhí)行 doOneThing,然后使用 ForkJoinPool.commonPool() 創(chuàng)建線程池(有關(guān) ForkJoinPool 的介紹見(jiàn) 這里),然后調(diào)用了線程池的 execute 方法把 futureTask 提交到線程池來(lái)執(zhí)行。

通過(guò)示例可以看到,雖然 FutureTask 提供了一些方法讓我們獲取任務(wù)的執(zhí)行結(jié)果、任務(wù)是否完成等,但是使用還是比較復(fù)雜,在一些較為復(fù)雜的場(chǎng)景(比如多個(gè) FutureTask 之間的關(guān)系表示)的編碼還是比較繁瑣,還是當(dāng)我們調(diào)用 getXXX() 系列方法時(shí)還是會(huì)在任務(wù)執(zhí)行完畢前阻塞調(diào)用線程,達(dá)不到異步編程的效果,基于這些問(wèn)題,在 JDK 8 中引入了 CompletableFuture 類(lèi),下面來(lái)看看如何使用 CompletableFuture 來(lái)實(shí)現(xiàn)異步編程。

CompletableFuture 方式

JDK 8 中引入了 CompletableFuture 類(lèi),實(shí)現(xiàn)了 FutureCompletionStage 接口,為異步編程提供了一些列方法,如 supplyAsyncrunAsyncthenApplyAsync 等,除此之外 CompletableFuture 還有一個(gè)重要的功能就是可以讓兩個(gè)或者多個(gè) CompletableFuture 進(jìn)行運(yùn)算來(lái)產(chǎn)生結(jié)果。代碼如下:

/**
 * @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();

    // 異步執(zhí)行版本
    testCompletableFuture();

    stopWatch.stop();
    System.out.println(stopWatch);
  }

  private static void testCompletableFuture() throws InterruptedException, ExecutionException {
    // 先執(zhí)行 doOneThing 任務(wù),后執(zhí)行 doOtherThing 任務(wù)
    CompletableFuture<String> resultFuture = doOneThing().thenCompose(CompletableFutureDemo::doOtherThing);

    // 獲取任務(wù)結(jié)果
    String doOneThingResult = resultFuture.get();

    // 獲取執(zhí)行結(jié)果
    System.out.println("DoOneThing and DoOtherThing execute finished. result = " + doOneThingResult);
  }

}

執(zhí)行結(jié)果如下:

4.png

在主線程 main 中首先調(diào)用了方法 doOneThing() 方法開(kāi)啟了一個(gè)異步任務(wù),并返回了對(duì)應(yīng)的 CompletableFuture 對(duì)象,我們?nèi)∶麨?doOneThingFuture,然后在 doOneThingFuture 的基礎(chǔ)上使用 CompletableFuturethenCompose() 方法,讓 doOneThingFuture 方法執(zhí)行完成后,使用其執(zhí)行結(jié)果作為 doOtherThing(String parameter) 方法的參數(shù)創(chuàng)建的異步任務(wù)返回。

我們不需要顯式使用 ExecutorService,在 CompletableFuture 內(nèi)部使用的是 Fork/Join 框架異步處理任務(wù),因此,它使我們編寫(xiě)的異步代碼更加簡(jiǎn)潔。此外,CompletableFuture 類(lèi)功能很強(qiáng)大其提供了和很多方便的方法,更多關(guān)于 CompletableFuture 的使用請(qǐng)見(jiàn) 這篇。

總結(jié)

本文介紹了在 Java 中的 JDK 使用異步編程的三種方式,這些是我們最基礎(chǔ)的實(shí)現(xiàn)異步編程的工具,在其之上的還有 Guava 庫(kù)提供的 ListenableFutureFutures 類(lèi)以及 Spring 框架提供的異步執(zhí)行能力,使用 @Async 等注解實(shí)現(xiàn)異步處理,感興趣的話可以自行學(xué)習(xí)了解。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容