除了異步請求,一般上我們用的比較多的應該是異步調用。通常在開發過程中,會遇到一個方法是和實際業務無關的,沒有緊密性的。比如記錄日志信息等業務。這個時候正常就是啟一個新線程去做一些業務處理,讓主線程異步的執行其他業務。
異步調用?
通常開發過程中,一般上我們都是同步調用,即:程序按定義的順序依次執行的過程,每一行代碼執行過程必須等待上一行代碼執行完畢后才執行。而異步調用指:程序在執行時,無需等待執行的返回值可繼續執行后面的代碼。顯而易見,同步有依賴相關性,而異步沒有,所以異步可並發執行,可提高執行效率,在相同的時間做更多的事情。
代碼
SpringBoot啟動類加上@EnableAsync注解
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
編寫同步調用和異步調用方法
@Async
public void asyncEvent() throws InterruptedException{
Thread.sleep(1000);
log.info("async---{}", System.currentTimeMillis());
}
@Override
public void syncEvent() throws InterruptedException{
Thread.sleep(1000);
log.info("sync---{}", System.currentTimeMillis());
}
編寫Controller調用
@Autowired
private AsyncService asyncService;
@GetMapping("async")
public Object doAsync() throws InterruptedException {
long startTime = System.currentTimeMillis();
log.info("方法開始執行---{}", startTime);
asyncService.syncEvent();
long syncTime = System.currentTimeMillis();
log.info("同步方法執行用時--{}", syncTime - startTime);
asyncService.asyncEvent();
long asyncTime = System.currentTimeMillis();
log.info("異步方法執行用時--{}", asyncTime - syncTime);
log.info("方法執行結束--{}", System.currentTimeMillis());
return "success";
}
調用打印日志為
2019-12-24 07:46:50.560 INFO 8904 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 方法開始執行---1577144810560
2019-12-24 07:46:51.560 INFO 8904 --- [nio-8080-exec-3] c.a.async.service.impl.AsyncServiceImpl : sync---1577144811560
2019-12-24 07:46:51.560 INFO 8904 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 同步方法執行用時--1000
2019-12-24 07:46:51.560 INFO 8904 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 異步方法執行用時--0
2019-12-24 07:46:51.560 INFO 8904 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 方法執行結束--1577144811560
2019-12-24 07:46:52.561 INFO 8904 --- [ task-3] c.a.async.service.impl.AsyncServiceImpl : async---1577144812561
注:在默認情況下,未設置TaskExecutor時,默認是使用SimpleAsyncTaskExecutor這個線程池,但此線程不是真正意義上的線程池,因為線程不重用,每次調用都會創建一個新的線程。可通過控制台日志輸出可以看出,每次輸出線程名都是遞增的。
調用的異步方法,不能為同一個類的方法,簡單來說,因為Spring在啟動掃描時會為其創建一個代理類,而同類調用時,還是調用本身的代理類的,所以和平常調用是一樣的。其他的注解如@Cache等也是一樣的道理,說白了,就是Spring的代理機制造成的。
如果在同一類的方法加上了異步調用注解
@GetMapping("asyncInOne")
public Object doAsyncInOne() throws InterruptedException {
long startTime = System.currentTimeMillis();
log.info("方法開始執行---{}", startTime);
asyncService.syncEvent();
long syncTime = System.currentTimeMillis();
log.info("同步方法執行用時--{}", syncTime - startTime);
asyncEventInOne();
long asyncTime = System.currentTimeMillis();
log.info("異步方法執行用時--{}", asyncTime - syncTime);
log.info("方法執行結束--{}", System.currentTimeMillis());
return "success";
}
@Async
public void asyncEventInOne() throws InterruptedException{
Thread.sleep(1000);
log.info("async-controller--{}", System.currentTimeMillis());
}
此時打印日志為:(可以看出並沒有進行異步調用)
2019-12-24 07:51:43.550 INFO 11724 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 方法開始執行---1577145103550
2019-12-24 07:51:44.551 INFO 11724 --- [nio-8080-exec-3] c.a.async.service.impl.AsyncServiceImpl : sync---1577145104551
2019-12-24 07:51:44.551 INFO 11724 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 同步方法執行用時--1001
2019-12-24 07:51:45.551 INFO 11724 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : async-controller--1577145105551
2019-12-24 07:51:45.551 INFO 11724 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 異步方法執行用時--1000
2019-12-24 07:51:45.551 INFO 11724 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 方法執行結束--1577145105551
自定義線程池
在默認情況下,系統使用的是默認的SimpleAsyncTaskExecutor進行線程創建。所以一般上我們會自定義線程池來進行線程的復用。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class ThreadConfig {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor getAsyncTheadPoolExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(20);
taskExecutor.setMaxPoolSize(200);
taskExecutor.setQueueCapacity(25);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("oKong-");
//線程池對拒絕任務(無可用線程的處理策略,目前只支持AbortPolicy、CallerRunPolicy:默認為后者)
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
------------------------
corePoolSize:線程池維護線程的最少數量
keepAliveSeconds:允許的空閑時間,當超過了核心線程出之外的線程在空閑時間到達之后會被銷毀
maxPoolSize:線程池維護線程的最大數量,只有在緩沖隊列滿了之后才會申請超過核心線程數的線程
queueCapacity:緩存隊列
rejectedExecutionHandler:線程池對拒絕任務(無線程可用)的處理策略。這里采用了CallerRunsPolicy策略,當線程池沒有處理能力的時候,該策略會直接在 execute 方法的調用線程中運行被拒絕的任務;如果執行程序已關閉,則會丟棄該任務。還有一個是AbortPolicy策略:處理程序遭到拒絕將拋出運行時RejectedExecutionException。
而在一些場景下,若需要在關閉線程池時等待當前調度任務完成后才開始關閉,可以通過簡單的配置,進行優雅的停機策略配置。關鍵就是通過setWaitForTasksToCompleteOnShutdown(true)和setAwaitTerminationSeconds方法。
setWaitForTasksToCompleteOnShutdown:表明等待所有線程執行完,默認為false。
setAwaitTerminationSeconds:等待的時間,因為不能無限的等待下去。
------------------------
異步注解加上我們的線程池名稱
@Async("asyncPoolTaskExecutor")
public void asyncEvent() throws InterruptedException{
Thread.sleep(1000);
log.info("async---{}", System.currentTimeMillis());
log.info("async---線程名稱{}", Thread.currentThread().getName());
}
此視再看打印的日志
2019-12-24 08:52:04.704 INFO 17824 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-12-24 08:52:04.704 INFO 17824 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2019-12-24 08:52:04.709 INFO 17824 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
2019-12-24 08:52:04.727 INFO 17824 --- [nio-8080-exec-1] c.agan.async.controller.AsyncController : 方法開始執行---1577148724727
2019-12-24 08:52:05.728 INFO 17824 --- [nio-8080-exec-1] c.a.async.service.impl.AsyncServiceImpl : sync---1577148725728
2019-12-24 08:52:05.728 INFO 17824 --- [nio-8080-exec-1] c.agan.async.controller.AsyncController : 同步方法執行用時--1001
2019-12-24 08:52:05.731 INFO 17824 --- [nio-8080-exec-1] c.agan.async.controller.AsyncController : 異步方法執行用時--3
2019-12-24 08:52:05.731 INFO 17824 --- [nio-8080-exec-1] c.agan.async.controller.AsyncController : 方法執行結束--1577148725731
2019-12-24 08:52:06.732 INFO 17824 --- [ task-1] c.a.async.service.impl.AsyncServiceImpl : async---1577148726732
2019-12-24 08:52:06.732 INFO 17824 --- [ task-1] c.a.async.service.impl.AsyncServiceImpl : async---線程名稱task-1
2019-12-24 08:52:11.910 INFO 17824 --- [nio-8080-exec-2] c.agan.async.controller.AsyncController : 方法開始執行---1577148731910
2019-12-24 08:52:12.910 INFO 17824 --- [nio-8080-exec-2] c.a.async.service.impl.AsyncServiceImpl : sync---1577148732910
2019-12-24 08:52:12.910 INFO 17824 --- [nio-8080-exec-2] c.agan.async.controller.AsyncController : 同步方法執行用時--1000
2019-12-24 08:52:12.910 INFO 17824 --- [nio-8080-exec-2] c.agan.async.controller.AsyncController : 異步方法執行用時--0
2019-12-24 08:52:12.910 INFO 17824 --- [nio-8080-exec-2] c.agan.async.controller.AsyncController : 方法執行結束--1577148732910
2019-12-24 08:52:13.910 INFO 17824 --- [ task-2] c.a.async.service.impl.AsyncServiceImpl : async---1577148733910
2019-12-24 08:52:13.910 INFO 17824 --- [ task-2] c.a.async.service.impl.AsyncServiceImpl : async---線程名稱task-2
2019-12-24 08:52:18.368 INFO 17824 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 方法開始執行---1577148738368
2019-12-24 08:52:19.369 INFO 17824 --- [nio-8080-exec-3] c.a.async.service.impl.AsyncServiceImpl : sync---1577148739369
2019-12-24 08:52:19.369 INFO 17824 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 同步方法執行用時--1001
2019-12-24 08:52:19.369 INFO 17824 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 異步方法執行用時--0
2019-12-24 08:52:19.369 INFO 17824 --- [nio-8080-exec-3] c.agan.async.controller.AsyncController : 方法執行結束--1577148739369
2019-12-24 08:52:20.369 INFO 17824 --- [ task-3] c.a.async.service.impl.AsyncServiceImpl : async---1577148740369
2019-12-24 08:52:20.369 INFO 17824 --- [ task-3] c.a.async.service.impl.AsyncServiceImpl : async---線程名稱task-3
異步回調與超時
對於一些業務場景下,需要異步回調的返回值時,就需要使用異步回調來完成了。主要就是通過Future進行異步回調。
@Async
public Future<String> asyncEventWithReturn() throws InterruptedException{
Thread.sleep(1000);
log.info("async---{}", System.currentTimeMillis());
log.info("async---線程名稱{}", Thread.currentThread().getName());
return new AsyncResult<>("異步方法返回");
}
其中AsyncResult是Spring提供的一個Future接口的子類。
然后通過isDone方法,判斷是否已經執行完畢。
@GetMapping("asyncWithReturn")
public Object doAsyncWithReturn() throws InterruptedException {
long startTime = System.currentTimeMillis();
log.info("方法開始執行---{}", startTime);
asyncService.syncEvent();
long syncTime = System.currentTimeMillis();
log.info("同步方法執行用時--{}", syncTime - startTime);
Future<String> stringFuture = asyncService.asyncEventWithReturn();
while (true) {
if (stringFuture.isDone()) {
break;
}
}
long asyncTime = System.currentTimeMillis();
log.info("異步方法執行用時--{}", asyncTime - syncTime);
log.info("方法執行結束--{}", System.currentTimeMillis());
return "success";
}
調用結果為
2019-12-24 09:26:28.835 INFO 5492 --- [nio-8080-exec-2] c.agan.async.controller.AsyncController : 方法開始執行---1577150788835
2019-12-24 09:26:29.837 INFO 5492 --- [nio-8080-exec-2] c.a.async.service.impl.AsyncServiceImpl : sync---1577150789837
2019-12-24 09:26:29.837 INFO 5492 --- [nio-8080-exec-2] c.agan.async.controller.AsyncController : 同步方法執行用時--1002
2019-12-24 09:26:30.840 INFO 5492 --- [ oKong-1] c.a.async.service.impl.AsyncServiceImpl : async---1577150790840
2019-12-24 09:26:30.840 INFO 5492 --- [ oKong-1] c.a.async.service.impl.AsyncServiceImpl : async---線程名稱oKong-1
2019-12-24 09:26:30.841 INFO 5492 --- [nio-8080-exec-2] c.agan.async.controller.AsyncController : 異步方法執行用時--1004
2019-12-24 09:26:30.841 INFO 5492 --- [nio-8080-exec-2] c.agan.async.controller.AsyncController : 方法執行結束--1577150790841
對於一些需要異步回調的函數,不能無期限的等待下去,所以一般上需要設置超時時間,超時后可將線程釋放,而不至於一直堵塞而占用資源。
try {
stringFuture.get(100, TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
log.error("超時");
e.printStackTrace();
}