日常開發中,會經常遇到說,前臺調服務,然後觸發一個比較耗時的非同步服務,且不用等非同步任務的處理結果就對原服務進行返回。這裡就涉及的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
有以下兩點需要注意:
- 類似
@Tranctional
註解,@Async
註解的方法與呼叫方法不能在同一個類中,否則不生效- 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