天天看點

Spring Boot 2.x基礎教程:配置@Async異步任務的線程池

上一篇我們介紹了 如何使用

@Async

注解來建立異步任務 ,我可以用這種方法來實作一些并發操作,以加速任務的執行效率。但是,如果隻是如前文那樣直接簡單的建立來使用,可能還是會碰到一些問題。存在有什麼問題呢?先來思考下,下面的這個接口,通過異步任務加速執行的實作,是否存在問題或風險呢?

@RestController
public class HelloController {

    @Autowired
    private AsyncTasks asyncTasks;
        
    @GetMapping("/hello")
    public String hello() {
        // 将可以并行的處理邏輯,拆分成三個異步任務同時執行
        CompletableFuture<String> task1 = asyncTasks.doTaskOne();
        CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
        CompletableFuture<String> task3 = asyncTasks.doTaskThree();
        
        CompletableFuture.allOf(task1, task2, task3).join();
        return "Hello World";
    }
}      

雖然,從單次接口調用來說,是沒有問題的。但當接口被用戶端頻繁調用的時候,異步任務的數量就會大量增長:3 x n(n為請求數量),如果任務處理不夠快,就很可能會出現記憶體溢出的情況。那麼為什麼會記憶體溢出呢?根本原因是由于Spring Boot預設用于異步任務的線程池是這樣配置的:

Spring Boot 2.x基礎教程:配置@Async異步任務的線程池

圖中我标出的兩個重要參數是需要關注的:

  • queueCapacity

    :緩沖隊列的容量,預設為INT的最大值(2的31次方-1)。
  • maxSize

    :允許的最大線程數,預設為INT的最大值(2的31次方-1)。

是以,預設情況下,一般任務隊列就可能把記憶體給堆滿了。是以,我們真正使用的時候,還需要對異步任務的執行線程池做一些基礎配置,以防止出現記憶體溢出導緻服務不可用的問題。

https://blog.didispace.com/spring-boot-learning-2-7-6/#%E9%85%8D%E7%BD%AE%E9%BB%98%E8%AE%A4%E7%BA%BF%E7%A8%8B%E6%B1%A0 配置預設線程池

預設線程池的配置很簡單,隻需要在配置檔案中完成即可,主要有以下這些參數:

spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.keep-alive=60s
spring.task.execution.pool.allow-core-thread-timeout=true
spring.task.execution.shutdown.await-termination=false
spring.task.execution.shutdown.await-termination-period=
spring.task.execution.thread-name-prefix=task-      

具體配置含義如下:

  • spring.task.execution.pool.core-size

    :線程池建立時的初始化線程數,預設為8
  • spring.task.execution.pool.max-size

    :線程池的最大線程數,預設為int最大值
  • spring.task.execution.pool.queue-capacity

    :用來緩沖執行任務的隊列,預設為int最大值
  • spring.task.execution.pool.keep-alive

    :線程終止前允許保持空閑的時間
  • spring.task.execution.pool.allow-core-thread-timeout

    :是否允許核心線程逾時
  • spring.task.execution.shutdown.await-termination

    :是否等待剩餘任務完成後才關閉應用
  • spring.task.execution.shutdown.await-termination-period

    :等待剩餘任務完成的最大時間
  • spring.task.execution.thread-name-prefix

    :線程名的字首,設定好了之後可以友善我們在日志中檢視處理任務所在的線程池

https://blog.didispace.com/spring-boot-learning-2-7-6/#%E5%8A%A8%E6%89%8B%E8%AF%95%E4%B8%80%E8%AF%95 動手試一試

我們直接基于之前

chapter7-5

的結果來進行如下操作。

首先,在沒有進行線程池配置之前,可以先執行一下單元測試:

@Test
public void test1() throws Exception {
    long start = System.currentTimeMillis();

    CompletableFuture<String> task1 = asyncTasks.doTaskOne();
    CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
    CompletableFuture<String> task3 = asyncTasks.doTaskThree();

    CompletableFuture.allOf(task1, task2, task3).join();

    long end = System.currentTimeMillis();

    log.info("任務全部完成,總耗時:" + (end - start) + "毫秒");
}      

由于預設線程池的核心線程數是8,是以3個任務會同時開始執行,日志輸出是這樣的:

2021-09-15 00:30:14.819  INFO 77614 --- [         task-2] com.didispace.chapter76.AsyncTasks       : 開始做任務二
2021-09-15 00:30:14.819  INFO 77614 --- [         task-3] com.didispace.chapter76.AsyncTasks       : 開始做任務三
2021-09-15 00:30:14.819  INFO 77614 --- [         task-1] com.didispace.chapter76.AsyncTasks       : 開始做任務一
2021-09-15 00:30:15.491  INFO 77614 --- [         task-2] com.didispace.chapter76.AsyncTasks       : 完成任務二,耗時:672毫秒
2021-09-15 00:30:19.496  INFO 77614 --- [         task-3] com.didispace.chapter76.AsyncTasks       : 完成任務三,耗時:4677毫秒
2021-09-15 00:30:20.443  INFO 77614 --- [         task-1] com.didispace.chapter76.AsyncTasks       : 完成任務一,耗時:5624毫秒
2021-09-15 00:30:20.443  INFO 77614 --- [           main] c.d.chapter76.Chapter76ApplicationTests  : 任務全部完成,總耗時:5653毫秒      

接着,可以嘗試在配置檔案中增加如下的線程池配置

2021-09-15 00:31:50.013  INFO 77985 --- [         task-1] com.didispace.chapter76.AsyncTasks       : 開始做任務一
2021-09-15 00:31:50.013  INFO 77985 --- [         task-2] com.didispace.chapter76.AsyncTasks       : 開始做任務二
2021-09-15 00:31:52.452  INFO 77985 --- [         task-1] com.didispace.chapter76.AsyncTasks       : 完成任務一,耗時:2439毫秒
2021-09-15 00:31:52.452  INFO 77985 --- [         task-1] com.didispace.chapter76.AsyncTasks       : 開始做任務三
2021-09-15 00:31:55.880  INFO 77985 --- [         task-2] com.didispace.chapter76.AsyncTasks       : 完成任務二,耗時:5867毫秒
2021-09-15 00:32:00.346  INFO 77985 --- [         task-1] com.didispace.chapter76.AsyncTasks       : 完成任務三,耗時:7894毫秒
2021-09-15 00:32:00.347  INFO 77985 --- [           main] c.d.chapter76.Chapter76ApplicationTests  : 任務全部完成,總耗時:10363毫秒      
  • 任務一和任務二會馬上占用核心線程,任務三進入隊列等待
  • 任務一完成,釋放出一個核心線程,任務三從隊列中移出,并占用核心線程開始處理
注意:這裡可能有的小夥伴會問,最大線程不是5麼,為什麼任務三是進緩沖隊列,不是建立新線程來處理嗎?這裡要了解緩沖隊列與最大線程間的關系:隻有在緩沖隊列滿了之後才會申請超過核心線程數的線程來進行處理。是以,這裡隻有緩沖隊列中10個任務滿了,再來第11個任務的時候,才會線上程池中建立第三個線程來處理。這個這裡就不具體寫列子了,讀者可以自己調整下參數,或者調整下單元測試來驗證這個邏輯。

好了,今天的學習就到這裡!如果您學習過程中如遇困難?可以加入我們超高品質的

Spring技術交流群

,參與交流與讨論,更好的學習與進步!更多

Spring Boot教程可以點選直達!

,歡迎收藏與轉發支援!

https://blog.didispace.com/spring-boot-learning-2-7-6/#%E4%BB%A3%E7%A0%81%E7%A4%BA%E4%BE%8B 代碼示例

本文的完整工程可以檢視下面倉庫中

2.x

目錄下的

chapter7-6

工程:

如果您覺得本文不錯,歡迎

Star

支援,您的關注是我堅持的動力!