SpringBoot 開發實踐(6):@Async 異步執行方法及配置自定義線程池

前言

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 許可協議。轉載請註明出處!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章