何爲異步調用
說異步調用
前,我們說說它對應的同步調用
。通常開發過程中,一般上我們都是同步調用
,即:程序按定義的順序依次執行的過程,每一行代碼執行過程必須等待上一行代碼執行完畢後才執行。而異步調用
指:程序在執行時,無需等待執行的返回值可繼續執行後面的代碼。顯而易見,同步有依賴相關性,而異步沒有,所以異步可併發
執行,可提高執行效率,在相同的時間做更多的事情。
題外話:處理異步
、同步
外,還有一個叫回調
。其主要是解決異步方法執行結果的處理方法,比如在希望異步調用結束時返回執行結果,這個時候就可以考慮使用回調機制。
Async異步調用
在SpringBoot
中使用異步調用是很簡單的,只需要使用@Async
註解即可實現方法的異步調用。
注意:需要在啓動類加入@EnableAsync
使異步調用@Async
註解生效。
1
2
3
4
5
6
7
8
9
10
|
@SpringBootApplication
@EnableAsync
@Slf4j
public class Chapter21Application {
public static void main(String[] args) {
SpringApplication.run(Chapter21Application. class , args);
log.info( "Chapter21啓動!" );
}
}
|
@Async異步調用
使用@Async
很簡單,只需要在需要異步執行的方法上加入此註解即可。這裏創建一個控制層和一個服務層,進行簡單示例下。
SyncService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@RestController
@Slf4j
public class AsyncController {
@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!!!" ;
}
}
|
應用啓動後,可以看見控制檯輸出:
1
2
3
4
5
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Configuration
public class Config {
/**
* 配置線程池
* @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
加入線程池名稱即可:
1
2
3
4
5
6
|
@Async ( "asyncPoolTaskExecutor" )
public void asyncEvent() throws InterruptedException {
//休眠1s
Thread.sleep( 1000 );
log.info( "異步方法內部線程名稱:{}!" , Thread.currentThread().getName());
}
|
再次啓動應用,就可以看見已經是使用自定義的線程了。
1
2
3
4
5
|
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:等待的時間,因爲不能無限的等待下去。
所以,線程池完整配置爲:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@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
。
1
2
3
4
5
6
7
|
@Async ( "asyncPoolTaskExecutor" )
public Future<String> asyncEvent() throws InterruptedException {
//休眠1s
Thread.sleep( 1000 );
log.info( "異步方法內部線程名稱:{}!" , Thread.currentThread().getName());
return new AsyncResult<>( "異步方法返回值" );
}
|
其中AsyncResult
是Spring
提供的一個Future
接口的子類。
然後通過isDone
方法,判斷是否已經執行完畢。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@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!!!" ;
}
|
此時,控制檯輸出:
1
2
3
4
5
|
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
方法即可,具體如下:
1
2
3
|
//get方法會一直堵塞,直到等待執行完成才返回
//get(long timeout, TimeUnit unit) 在設置時間類未返回結果,會直接排除異常TimeoutException,messages爲null
String result = doFutrue.get( 60 , TimeUnit.SECONDS); //60s
|
超時後,會拋出異常TimeoutException
類,此時可進行統一異常捕獲即可。