前言
SpringBoot 中的方法調用,默認是單線程順序執行的。但是在開發中我們可能會存在這樣一些場景,例如發送郵件或者記錄日誌等,這些操作往往比較耗時,但是又不是主業務中跟業務相關的內容。這種場景我們就可以選擇使用 @Async
異步方法執行,即用其它線程來異步執行某些耗時操作,從而節省主線程的運行等待時間。
使用 @Async 異步執行方法
想要使方法異步執行非常簡單,簡單來說,只需要在需要異步執行的方法上添加 @Async
註解即可。
編寫一個 @Service
服務類,模擬耗時操作。在方法的前後,我們打上開始和結束日誌,並輸出線程名。
@Service
public class AsyncServiceImpl implements AsyncService {
private static final Logger LOG = LoggerFactory.getLogger(AsyncServiceImpl.class);
@Async
@Override
public void printLog1() {
LOG.info("printLog1 開始執行 -> Thread name is: {}", Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOG.info("printLog1 執行完畢 -> Thread name is: {}", Thread.currentThread().getName());
}
}
@Async: 表明該方法異步執行。
注意: 異步方法和調用該異步方法的方法不能放在同一個類中,否則 @Async
註解將失效。例如,方法 A 和異步方法 B 都在同一個類中,那麼 A 中調用 B 時,B 還是會按照單線程來運行。解決方法就是,將 A、B 拆開,放在兩個類中。
編寫一個定時任務,定時執行該方法。
@Component
public class SchedulerTask {
private static final Logger LOG = LoggerFactory.getLogger(SchedulerTask.class);
@Autowired
private AsyncService asyncService;
/**
* 每秒執行一次
*/
@Scheduled(cron = "*/5 * * * * ?")
public void scheduler1() {
LOG.info("scheduler1 開始執行");
asyncService.printLog1();
LOG.info("scheduler1 執行完畢");
}
}
最後,別忘了在 Application 入口類上打上 @EnableAsync
註解,用於開始異步執行功能。
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class AsyncExecutionApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncExecutionApplication.class, args);
}
}
啓動程序,我們可以看到如下日誌:
2020-06-22 21:57:30.001 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 開始執行
2020-06-22 21:57:30.018 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 執行完畢
2020-06-22 21:57:30.019 INFO 57067 --- [TaskExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 開始執行 -> Thread name is: SimpleAsyncTaskExecutor-1
2020-06-22 21:57:33.020 INFO 57067 --- [TaskExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 執行完畢 -> Thread name is: SimpleAsyncTaskExecutor-1
2020-06-22 21:57:35.003 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 開始執行
2020-06-22 21:57:35.004 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 執行完畢
2020-06-22 21:57:35.004 INFO 57067 --- [TaskExecutor-2] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 開始執行 -> Thread name is: SimpleAsyncTaskExecutor-2
2020-06-22 21:57:38.008 INFO 57067 --- [TaskExecutor-2] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 執行完畢 -> Thread name is: SimpleAsyncTaskExecutor-2
2020-06-22 21:57:40.001 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 開始執行
2020-06-22 21:57:40.001 INFO 57067 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler1 執行完畢
2020-06-22 21:57:40.002 INFO 57067 --- [TaskExecutor-3] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 開始執行 -> Thread name is: SimpleAsyncTaskExecutor-3
2020-06-22 21:57:43.003 INFO 57067 --- [TaskExecutor-3] c.i.s.a.service.impl.AsyncServiceImpl : printLog1 執行完畢 -> Thread name is: SimpleAsyncTaskExecutor-3
我們可以看到,定時任務與 printLog1()
方法併發執行,說明 printLog1()
方法成功地異步執行了。
自定義線程池
從上面的日誌我們可以看到,使用默認 @Async
異步執行的方法,用的是 SimpleAsyncTaskExecutor
不重用線程,每次調用都創建了一個新的線程。
默認的 @Async
雖然可以應付一般的場景,但是如果是併發量比較高的情況下,就存在一定風險了。例如開銷過大、內存溢出等。爲使服務運行穩定,我們可以自定義配置線程池,然後讓給需要異步執行的方法指定用該線程池運行。
配置自定義線程池
創建一個 ExecutorConfig.java
配置類。
@Configuration
public class ExecutorConfig {
@Bean(name = "myExecutor")
public Executor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心線程數
executor.setCorePoolSize(3);
//配置最大線程數
executor.setMaxPoolSize(10);
//配置隊列大小
executor.setQueueCapacity(100);
//配置線程池中的線程的名稱前綴
executor.setThreadNamePrefix("MyExecutor-");
// rejection-policy:當pool已經達到max size的時候,如何處理新任務
// CALLER_RUNS:不在新線程中執行任務,而是有調用者所在的線程來執行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//執行初始化
executor.initialize();
return executor;
}
}
@Configuration: 表名這是一個配置類。該類中打上 @Bean
註解的方法都會在 Spring 啓動時被掃描運行,然後將返回的 bean 注入到 Spring 容器中。
@Bean: 該方法返回的對象將被注入到 Spring 容器中。在上面的方法中,我們將自定義配置的線程池命名爲 myExecutor
交給 Spring 來管理。
給異步方法指定線程池
我們再創建一個異步方法,這次將該方法指定給我們剛剛配置好的線程池來處理。
@Async("myExecutor")
@Override
public void printLog2() {
LOG.info("printLog2 開始執行 -> Thread name is: {}", Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOG.info("printLog2 執行完畢 -> Thread name is: {}", Thread.currentThread().getName());
}
@Async: 將自定義線程池的名字賦值給 @Async
,那麼就表明該方法需要用 myExecutor
線程池來處理。
啓動程序,運行結果如下:
2020-06-23 01:19:40.001 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 開始執行
2020-06-23 01:19:40.018 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 執行完畢
2020-06-23 01:19:40.018 INFO 57595 --- [ MyExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 開始執行 -> Thread name is: MyExecutor-1
2020-06-23 01:19:43.022 INFO 57595 --- [ MyExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 執行完畢 -> Thread name is: MyExecutor-1
2020-06-23 01:19:45.000 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 開始執行
2020-06-23 01:19:45.001 INFO 57595 --- [ MyExecutor-2] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 開始執行 -> Thread name is: MyExecutor-2
2020-06-23 01:19:45.001 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 執行完畢
2020-06-23 01:19:48.002 INFO 57595 --- [ MyExecutor-2] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 執行完畢 -> Thread name is: MyExecutor-2
2020-06-23 01:19:50.004 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 開始執行
2020-06-23 01:19:50.005 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 執行完畢
2020-06-23 01:19:50.005 INFO 57595 --- [ MyExecutor-3] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 開始執行 -> Thread name is: MyExecutor-3
2020-06-23 01:19:53.006 INFO 57595 --- [ MyExecutor-3] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 執行完畢 -> Thread name is: MyExecutor-3
2020-06-23 01:19:55.001 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 開始執行
2020-06-23 01:19:55.002 INFO 57595 --- [ MyExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 開始執行 -> Thread name is: MyExecutor-1
2020-06-23 01:19:55.002 INFO 57595 --- [ scheduling-1] c.i.s.a.scheduler.SchedulerTask : scheduler2 執行完畢
2020-06-23 01:19:58.003 INFO 57595 --- [ MyExecutor-1] c.i.s.a.service.impl.AsyncServiceImpl : printLog2 執行完畢 -> Thread name is: MyExecutor-1
可以看到,日誌中輸出的線程前綴名稱,即爲我們自定義線程池前綴的名稱。
以上就是使用 @Async 異步執行及配置自定義線程池的方法。
本章代碼地址:GitHub
我是因特馬,一個愛分享的斜槓程序員~
歡迎關注我的公衆號:一隻因特馬
原文作者: 一隻因特馬
原文鏈接: https://www.interhorse.cn/a/3350135757/
版權聲明: 本博客所有文章除特別聲明外,均採用 BY-NC-ND 許可協議。轉載請註明出處!