1. 程式人生 > >SpringBoot | :異步開發之異步調用

SpringBoot | :異步開發之異步調用

調用 3.6 prefix 名稱 interrupt keepalive wired for 最大

前言

除了異步請求,一般上我們用的比較多的應該是異步調用。通常在開發過程中,會遇到一個方法是和實際業務無關的,沒有緊密性的。比如記錄日誌信息等業務。這個時候正常就是啟一個新線程去做一些業務處理,讓主線程異步的執行其他業務。所以,本章節重點說下在SpringBoot中如何進行異步調用及其相關知識和註意點。

何為異步調用

說異步調用前,我們說說它對應的同步調用。通常開發過程中,一般上我們都是同步調用,即:程序按定義的順序依次執行的過程,每一行代碼執行過程必須等待上一行代碼執行完畢後才執行。而異步調用指:程序在執行時,無需等待執行的返回值可繼續執行後面的代碼。顯而易見,同步有依賴相關性,而異步沒有,所以異步可並發執行,可提高執行效率,在相同的時間做更多的事情。

題外話:處理異步、同步外,還有一個叫回調。其主要是解決異步方法執行結果的處理方法,比如在希望異步調用結束時返回執行結果,這個時候就可以考慮使用回調機制。

Async異步調用

在SpringBoot中使用異步調用是很簡單的,只需要使用@Async註解即可實現方法的異步調用。

註意:需要在啟動類加入@EnableAsync使異步調用@Async註解生效。

@SpringBootApplication@EnableAsync
br/>@EnableAsync
public class Chapter21Application {

public static void main(String[] args) {
    SpringApplication.run(Chapter21Application.class, args);
    log.info("Chapter21啟動!");
}

}

@Async異步調用

使用@Async很簡單,只需要在需要異步執行的方法上加入此註解即可。這裏創建一個控制層和一個服務層,進行簡單示例下。

SyncService.java

@Component
public class SyncService {

@Async
public void asyncEvent() throws InterruptedException {
    //休眠1s
    Thread.sleep(1000);
    //log.info("異步方法輸出:{}!", System.currentTimeMillis());
}

public void syncEvent() throws InterruptedException {
    Thread.sleep(1000);
    //log.info("同步方法輸出:{}!", System.currentTimeMillis());
}

}
控制層:AsyncController.java

@RestController@Slf4j
br/>@Slf4j

@Autowired
SyncService syncService;

@GetMapping("/async")
public String doAsync() throws InterruptedException {
    long start = System.currentTimeMillis();
    log.info("方法執行開始:{}", start);
    //調用同步方法
    syncService.syncEvent();
    long syncTime = System.currentTimeMillis();
    log.info("同步方法用時:{}", syncTime - start);
    //調用異步方法
    syncService.asyncEvent();
    long asyncTime = System.currentTimeMillis();
    log.info("異步方法用時:{}", asyncTime - syncTime);
    log.info("方法執行完成:{}!",asyncTime);
    return "async!!!";
}

}
應用啟動後,可以看見控制臺輸出:
2018-08-16 22:21:35.949 INFO 17152 --- [nio-8080-exec-5] c.l.l.s.c.controller.AsyncController : 方法執行開始:1534429295949
2018-08-16 22:21:36.950 INFO 17152 --- [nio-8080-exec-5] c.l.l.s.c.controller.AsyncController : 同步方法用時:1001
2018-08-16 22:21:36.950 INFO 17152 --- [nio-8080-exec-5] c.l.l.s.c.controller.AsyncController : 異步方法用時:0
2018-08-16 22:21:36.950 INFO 17152 --- [nio-8080-exec-5] c.l.l.s.c.controller.AsyncController : 方法執行完成:1534429296950!
2018-08-16 22:21:37.950 INFO 17152 --- [cTaskExecutor-3] c.l.l.s.chapter21.service.SyncService : 異步方法內部線程名稱:SimpleAsyncTaskExecutor-3!
可以看出,調用異步方法時,是立即返回的,基本沒有耗時。

這裏有幾點需要註意下:

在默認情況下,未設置TaskExecutor時,默認是使用SimpleAsyncTaskExecutor這個線程池,但此線程不是真正意義上的線程池,因為線程不重用,每次調用都會創建一個新的線程。可通過控制臺日誌輸出可以看出,每次輸出線程名都是遞增的。
調用的異步方法,不能為同一個類的方法,簡單來說,因為Spring在啟動掃描時會為其創建一個代理類,而同類調用時,還是調用本身的代理類的,所以和平常調用是一樣的。其他的註解如@Cache等也是一樣的道理,說白了,就是Spring的代理機制造成的。

自定義線程池

前面有提到,在默認情況下,系統使用的是默認的SimpleAsyncTaskExecutor進行線程創建。所以一般上我們會自定義線程池來進行線程的復用。

創建一個自定義的ThreadPoolTaskExecutor線程池:Config.java
@Configuration
br/>Config.java
@Configuration

/**
 * 配置線程池
 * @return
 */
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(20);
    taskExecutor.setMaxPoolSize(200);
    taskExecutor.setQueueCapacity(25);
    taskExecutor.setKeepAliveSeconds(200);
    taskExecutor.setThreadNamePrefix("oKong-");
    // 線程池對拒絕任務(無線程可用)的處理策略,目前只支持AbortPolicy、CallerRunsPolicy;默認為後者
    taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    taskExecutor.initialize();
    return taskExecutor;
}

}
此時,使用的是就只需要在@Async加入線程池名稱即可:

@Async("asyncPoolTaskExecutor")
public void asyncEvent() throws InterruptedException {
//休眠1s
Thread.sleep(1000);
log.info("異步方法內部線程名稱:{}!", Thread.currentThread().getName());
}
再次啟動應用,就可以看見已經是使用自定義的線程了。
2018-08-16 22:32:02.676 INFO 4516 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 方法執行開始:1534429922676
2018-08-16 22:32:03.681 INFO 4516 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 同步方法用時:1005
2018-08-16 22:32:03.693 INFO 4516 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 異步方法用時:12
2018-08-16 22:32:03.693 INFO 4516 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 方法執行完成:1534429923693!
2018-08-16 22:32:04.694 INFO 4516 --- [ oKong-1] c.l.l.s.chapter21.service.SyncService : 異步方法內部線程名稱:oKong-1!
這裏簡單說明下,關於ThreadPoolTaskExecutor參數說明:

corePoolSize:線程池維護線程的最少數量
keepAliveSeconds:允許的空閑時間,當超過了核心線程出之外的線程在空閑時間到達之後會被銷毀
maxPoolSize:線程池維護線程的最大數量,只有在緩沖隊列滿了之後才會申請超過核心線程數的線程
queueCapacity:緩存隊列
rejectedExecutionHandler:線程池對拒絕任務(無線程可用)的處理策略。這裏采用了CallerRunsPolicy策略,當線程池沒有處理能力的時候,該策略會直接在 execute 方法的調用線程中運行被拒絕的任務;如果執行程序已關閉,則會丟棄該任務。還有一個是AbortPolicy策略:處理程序遭到拒絕將拋出運行時RejectedExecutionException。
而在一些場景下,若需要在關閉線程池時等待當前調度任務完成後才開始關閉,可以通過簡單的配置,進行優雅的停機策略配置。關鍵就是通過setWaitForTasksToCompleteOnShutdown(true)和setAwaitTerminationSeconds方法。

setWaitForTasksToCompleteOnShutdown:表明等待所有線程執行完,默認為false。
setAwaitTerminationSeconds:等待的時間,因為不能無限的等待下去。
所以,線程池完整配置為:
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(20);
taskExecutor.setMaxPoolSize(200);
taskExecutor.setQueueCapacity(25);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("oKong-");
// 線程池對拒絕任務(無線程可用)的處理策略,目前只支持AbortPolicy、CallerRunsPolicy;默認為後者
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//調度器shutdown被調用時等待當前被調度的任務完成
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
//等待時長
taskExecutor.setAwaitTerminationSeconds(60);
taskExecutor.initialize();
return taskExecutor;
}

異步回調及超時處理

對於一些業務場景下,需要異步回調的返回值時,就需要使用異步回調來完成了。主要就是通過Future進行異步回調。

異步回調
修改下異步方法的返回類型,加入Future。

@Async("asyncPoolTaskExecutor")
public Future<String> asyncEvent() throws InterruptedException {
//休眠1s
Thread.sleep(1000);
log.info("異步方法內部線程名稱:{}!", Thread.currentThread().getName());
return new AsyncResult<>("異步方法返回值");
}
其中AsyncResult是Spring提供的一個Future接口的子類。

然後通過isDone方法,判斷是否已經執行完畢。

@GetMapping("/async")
public String doAsync() throws InterruptedException {
long start = System.currentTimeMillis();
log.info("方法執行開始:{}", start);
//調用同步方法
syncService.syncEvent();
long syncTime = System.currentTimeMillis();
log.info("同步方法用時:{}", syncTime - start);
//調用異步方法
Future<String> doFutrue = syncService.asyncEvent();
while(true) {
//判斷異步任務是否完成
if(doFutrue.isDone()) {
break;
}
Thread.sleep(100);
}
long asyncTime = System.currentTimeMillis();
log.info("異步方法用時:{}", asyncTime - syncTime);
log.info("方法執行完成:{}!",asyncTime);
return "async!!!";
}
此時,控制臺輸出:
2018-08-16 23:10:57.021 INFO 9072 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 方法執行開始:1534431237020
2018-08-16 23:10:58.025 INFO 9072 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 同步方法用時:1005
2018-08-16 23:10:59.037 INFO 9072 --- [ oKong-1] c.l.l.s.chapter21.service.SyncService : 異步方法內部線程名稱:oKong-1!
2018-08-16 23:10:59.040 INFO 9072 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 異步方法用時:1015
2018-08-16 23:10:59.040 INFO 9072 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 方法執行完成:1534431239040!
所以,當某個業務功能可以同時拆開一起執行時,可利用異步回調機制,可有效的減少程序執行時間,提高效率。

超時處理
對於一些需要異步回調的函數,不能無期限的等待下去,所以一般上需要設置超時時間,超時後可將線程釋放,而不至於一直堵塞而占用資源。

對於Future配置超時,很簡單,通過get方法即可,具體如下:
//get方法會一直堵塞,直到等待執行完成才返回
//get(long timeout, TimeUnit unit) 在設置時間類未返回結果,會直接排除異常TimeoutException,messages為null
String result = doFutrue.get(60, TimeUnit.SECONDS);//60s
超時後,會拋出異常TimeoutException類,此時可進行統一異常捕獲即可。
http://qiniu.xds123.cn/18-8-16/35438012.jpg

總結

本章節主要是講解了異步請求的使用及相關配置,如超時,異常等處理。在剝離一些和業務無關的操作時,就可以考慮使用異步調用進行其他無關業務操作,以此提供業務的處理效率。或者一些業務場景下可拆分出多個方法進行同步執行又互不影響時,也可以考慮使用異步調用方式提供執行效率。
最後
目前互聯網上很多大佬都有SpringBoot系列教程,如有雷同,請多多包涵了。若文中有所錯誤之處,還望提出,謝謝。
歡迎工作一到五年的Java工程師朋友們加入Java架構開發:798891710

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導
同時大家可以多多關註一下小編公眾號:Java架構師秘籍 純幹貨 大家一起學習進步

SpringBoot | :異步開發之異步調用