close
關注我,回復關鍵字「spring」,
免費領取Spring學習資料。

上一篇我們介紹了如何使用@Async註解來創建異步任務,我可以用這種方法來實現一些並發操作,以加速任務的執行效率。但是,如果只是如前文那樣直接簡單的創建來使用,可能還是會碰到一些問題。存在有什麼問題呢?先來思考下,下面的這個接口,通過異步任務加速執行的實現,是否存在問題或風險呢?

@RestControllerpublicclassHelloController{@AutowiredprivateAsyncTasksasyncTasks;@GetMapping("/hello")publicStringhello(){//將可以並行的處理邏輯,拆分成三個異步任務同時執行CompletableFuture<String>task1=asyncTasks.doTaskOne();CompletableFuture<String>task2=asyncTasks.doTaskTwo();CompletableFuture<String>task3=asyncTasks.doTaskThree();CompletableFuture.allOf(task1,task2,task3).join();return"HelloWorld";}}

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

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

queueCapacity:緩衝隊列的容量,默認為INT的最大值(2的31次方-1)。
maxSize:允許的最大線程數,默認為INT的最大值(2的31次方-1)。

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

配置默認線程池

默認線程池的配置很簡單,只需要在配置文件中完成即可,主要有以下這些參數:

spring.task.execution.pool.core-size=2spring.task.execution.pool.max-size=5spring.task.execution.pool.queue-capacity=10spring.task.execution.pool.keep-alive=60sspring.task.execution.pool.allow-core-thread-timeout=truespring.task.execution.shutdown.await-termination=falsespring.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:線程名的前綴,設置好了之後可以方便我們在日誌中查看處理任務所在的線程池
動手試一試

我們直接基於之前chapter7-5的結果來進行如下操作。

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

@Testpublicvoidtest1()throwsException{longstart=System.currentTimeMillis();CompletableFuture<String>task1=asyncTasks.doTaskOne();CompletableFuture<String>task2=asyncTasks.doTaskTwo();CompletableFuture<String>task3=asyncTasks.doTaskThree();CompletableFuture.allOf(task1,task2,task3).join();longend=System.currentTimeMillis();log.info("任務全部完成,總耗時:"+(end-start)+"毫秒");}

由於默認線程池的核心線程數是8,所以3個任務會同時開始執行,日誌輸出是這樣的:

2021-09-1500:30:14.819INFO77614---[task-2]com.didispace.chapter76.AsyncTasks:開始做任務二2021-09-1500:30:14.819INFO77614---[task-3]com.didispace.chapter76.AsyncTasks:開始做任務三2021-09-1500:30:14.819INFO77614---[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毫秒

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

spring.task.execution.pool.core-size=2spring.task.execution.pool.max-size=5spring.task.execution.pool.queue-capacity=10spring.task.execution.pool.keep-alive=60sspring.task.execution.pool.allow-core-thread-timeout=truespring.task.execution.thread-name-prefix=task-

日誌輸出的順序會變成如下的順序:

2021-09-1500:31:50.013INFO77985---[task-1]com.didispace.chapter76.AsyncTasks:開始做任務一2021-09-1500:31:50.013INFO77985---[task-2]com.didispace.chapter76.AsyncTasks:開始做任務二2021-09-15 00:31:52.452 INFO 77985 ---[ task-1] com.didispace.chapter76.AsyncTasks :完成任務一,耗時:2439毫秒2021-09-1500:31:52.452INFO77985---[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個任務的時候,才會在線程池中創建第三個線程來處理。這個這裡就不具體寫列子了,讀者可以自己調整下參數,或者調整下單元測試來驗證這個邏輯。

代碼示例

本文的完整工程可以查看下面倉庫中2.x目錄下的chapter7-6工程:

Github:https://github.com/dyc87112/SpringBoot-Learning
Gitee:https://gitee.com/didispace/SpringBoot-Learning

如果您覺得本文不錯,歡迎Star支持,您的關注是我堅持的動力!



END



公司規定所有接口都用 POST請求?
一次值得學習的 Code Review 分享
IDEA調試技巧:給斷點加條件
靈魂畫手:圖解Spring AOP實現原理
當字節跳動在美國輸出中國式996...

關注後端面試那些事,回復【2022面經】

獲取最新大廠Java面經


最後重要提示:高質量的技術交流群,限時免費開放,今年抱團最重要。想進群的,關注SpringForAll社區,回復關鍵詞:加群,拉你進群。




點擊「閱讀原文」領取2022大廠面經
↓↓↓
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()