說說Java異步調用的幾種方式

日常開發中,會經常遇到說,前台調服務,然後觸發一個比較耗時的異步服務,且不用等異步任務的處理結果就對原服務進行返回。這裡就涉及的Java異步調用的一個知識。下面本文嘗試將Java異步調用的多種方式進行歸納。

一、通過創建新線程


首先的我們得認識到,異步調用的本質,其實是通過開啟一個新的線程來執行。如以下例子:

public static void main(String[] args) throws Exception{

    System.out.println("主線程 =====> 開始 =====> " + System.currentTimeMillis());

    new Thread(() -> {
        System.out.println("異步線程 =====> 開始 =====> " + System.currentTimeMillis());
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("異步線程 =====> 結束 =====> " + System.currentTimeMillis());
    }).start();

    Thread.sleep(2000);

    System.out.println("主線程 =====> 結束 =====> " + System.currentTimeMillis());
    
}

數據結果如下所示,我們知道,System.currentTimeMillis()時間單位為ms。

主線程 =====> 開始 =====> 1627893837146
異步線程 =====> 開始 =====> 1627893837200
主線程 =====> 結束 =====> 1627893839205
異步線程 =====> 結束 =====> 1627893842212

我們通過線程休眠來達成主線程執行時間2秒左右,異步線程執行5秒左右的效果。通過打印出來的時間戳倒數第四位(秒位)我們可以看出,兩個的線程執行總時間為5秒左右,符合異步執行的特徵

以上是採用Runable實現多線程創建方式的lambda寫法,關於的lambda知識,可參考Java Lambda 表達式;而關於多線程的多種實現方式,Java多線程事務管理一文有提及,可移步查看

二、通過線程池


因為異步任務的實現本質的由新線程來執行任務,所以通過線程池的也可以實現異步執行。寫法同我們利用線程池開啟多線程一樣。但由於我們的目的不是執行多線程,而是異步執行任務,所以一般需要另外一個線程就夠了。

因此區別於執行多線程任務的我們常用的newFixedThreadPool,在執行異步任務時,我們用newSingleThreadExecutor 來創建一個單個線程的線程池。

public static void main(String[] args) throws Exception{

    System.out.println("主線程 =====> 開始 =====> " + System.currentTimeMillis());

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.submit(()->{
        System.out.println("異步線程 =====> 開始 =====> " + System.currentTimeMillis());
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("異步線程 =====> 結束 =====> " + System.currentTimeMillis());
    });
    executorService.shutdown(); // 回收線程池

    Thread.sleep(2000);

    System.out.println("主線程 =====> 結束 =====> " + System.currentTimeMillis());
    
}

執行結果如下:

主線程 =====> 開始 =====> 1627895467578
異步線程 =====> 開始 =====> 1627895467635
主線程 =====> 結束 =====> 1627895469644
異步線程 =====> 結束 =====> 1627895472649

可以看到,結果跟第一種結果是基本一致的。

溫馨提示:不要忘記線程池的回收

三、通過@Async註解


我們都知道,SpringBoot項目有一個的很重要的特點就是的註解化。如果你的項目是SpringBoot,那就又多了一種選擇——@Async註解。

使用起來也非常簡單,將要異步執行的代碼封裝成一個方法,然後用@Async註解該方法,然後在主方法中直接調用就行。

@Test
public void mainThread() throws Exception{

    System.out.println("主線程 =====> 開始 =====> " + System.currentTimeMillis());
    collectionBill.asyncThread();
    Thread.sleep(2000);
    System.out.println("主線程 =====> 結束 =====> " + System.currentTimeMillis());

    Thread.sleep(4000); // 用於防止jvm停止,導致異步線程中斷
}
@Async
public void asyncThread(){
    System.out.println("異步線程 =====> 開始 =====> " + System.currentTimeMillis());
    try{
        Thread.sleep(5000);
    }catch (InterruptedException e){
        e.printStackTrace();
    }
    System.out.println("異步線程 =====> 結束 =====> " + System.currentTimeMillis());
}

執行結果如下:

主線程 =====> 開始 =====> 1627897539948
異步線程 =====> 開始 =====> 1627897539956
主線程 =====> 結束 =====> 1627897541965
異步線程 =====> 結束 =====> 1627897544966

有以下兩點需要注意:

  1. 類似@Tranctional註解,@Async註解的方法與調用方法不能在同一個類中,否則不生效
  2. JUnit框架的設計不考慮多線程場景,所以主線程退出後,子線程也會跟着立即退出,所以可以在後面加多線程休眠時間來觀察異步線程的執行情況

四、通過CompletableFuture


CompletableFuture是JDK1.8的新特性,是對Future的擴展。CompletableFuture實現了CompletionStage接口和Future接口,增加了異步回調、流式處理、多個Future組合處理的能力。

實現代碼如下:

public static void main(String[] args) throws Exception{

    System.out.println("主線程 =====> 開始 =====> " + System.currentTimeMillis());

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    CompletableFuture.runAsync(() ->{
        System.out.println("異步線程 =====> 開始 =====> " + System.currentTimeMillis());
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("異步線程 =====> 結束 =====> " + System.currentTimeMillis());
    },executorService);
    executorService.shutdown(); // 回收線程池

    Thread.sleep(2000);

    System.out.println("主線程 =====> 結束 =====> " + System.currentTimeMillis());
    
}

同樣可以實現類似的結果如下:

主線程 =====> 開始 =====> 1627898354914
異步線程 =====> 開始 =====> 1627898354977
主線程 =====> 結束 =====> 1627898356980
異步線程 =====> 結束 =====> 1627898359979

CompletableFuture有者非常強大的功能,能給我們帶來非常絲滑的編程體驗。後續會寫一篇文章來詳細介紹CompletableFuture