什么?用@Async會(huì)內(nèi)存溢出?看看你的線程池配置了沒(méi)!
上一篇我們介紹了如何使用@Async注解來(lái)創(chuàng)建異步任務(wù),我可以用這種方法來(lái)實(shí)現(xiàn)一些并發(fā)操作,以加速任務(wù)的執(zhí)行效率。但是,如果只是如前文那樣直接簡(jiǎn)單的創(chuàng)建來(lái)使用,可能還是會(huì)碰到一些問(wèn)題。存在有什么問(wèn)題呢?先來(lái)思考下,下面的這個(gè)接口,通過(guò)異步任務(wù)加速執(zhí)行的實(shí)現(xiàn),是否存在問(wèn)題或風(fēng)險(xiǎn)呢?
- @RestController
- public class HelloController {
- @Autowired
- private AsyncTasks asyncTasks;
- @GetMapping("/hello")
- public String hello() {
- // 將可以并行的處理邏輯,拆分成三個(gè)異步任務(wù)同時(shí)執(zhí)行
- CompletableFuture<String> task1 = asyncTasks.doTaskOne();
- CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
- CompletableFuture<String> task3 = asyncTasks.doTaskThree();
- CompletableFuture.allOf(task1, task2, task3).join();
- return "Hello World";
- }
- }
雖然,從單次接口調(diào)用來(lái)說(shuō),是沒(méi)有問(wèn)題的。但當(dāng)接口被客戶端頻繁調(diào)用的時(shí)候,異步任務(wù)的數(shù)量就會(huì)大量增長(zhǎng):3 x n(n為請(qǐng)求數(shù)量),如果任務(wù)處理不夠快,就很可能會(huì)出現(xiàn)內(nèi)存溢出的情況。那么為什么會(huì)內(nèi)存溢出呢?根本原因是由于Spring Boot默認(rèn)用于異步任務(wù)的線程池是這樣配置的:
圖中我標(biāo)出的兩個(gè)重要參數(shù)是需要關(guān)注的:
- queueCapacity:緩沖隊(duì)列的容量,默認(rèn)為INT的最大值(2的31次方-1)。
- maxSize:允許的最大線程數(shù),默認(rèn)為INT的最大值(2的31次方-1)。
所以,默認(rèn)情況下,一般任務(wù)隊(duì)列就可能把內(nèi)存給堆滿了。所以,我們真正使用的時(shí)候,還需要對(duì)異步任務(wù)的執(zhí)行線程池做一些基礎(chǔ)配置,以防止出現(xiàn)內(nèi)存溢出導(dǎo)致服務(wù)不可用的問(wèn)題。
配置默認(rèn)線程池
默認(rèn)線程池的配置很簡(jiǎn)單,只需要在配置文件中完成即可,主要有以下這些參數(shù):
- 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:線程池創(chuàng)建時(shí)的初始化線程數(shù),默認(rèn)為8
- spring.task.execution.pool.max-size:線程池的最大線程數(shù),默認(rèn)為int最大值
- spring.task.execution.pool.queue-capacity:用來(lái)緩沖執(zhí)行任務(wù)的隊(duì)列,默認(rèn)為int最大值
- spring.task.execution.pool.keep-alive:線程終止前允許保持空閑的時(shí)間
- spring.task.execution.pool.allow-core-thread-timeout:是否允許核心線程超時(shí)
- spring.task.execution.shutdown.await-termination:是否等待剩余任務(wù)完成后才關(guān)閉應(yīng)用
- spring.task.execution.shutdown.await-termination-period:等待剩余任務(wù)完成的最大時(shí)間
- spring.task.execution.thread-name-prefix:線程名的前綴,設(shè)置好了之后可以方便我們?cè)谌罩局胁榭刺幚砣蝿?wù)所在的線程池
動(dòng)手試一試
我們直接基于之前chapter7-5的結(jié)果來(lái)進(jìn)行如下操作。
首先,在沒(méi)有進(jìn)行線程池配置之前,可以先執(zhí)行一下單元測(cè)試:
- @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("任務(wù)全部完成,總耗時(shí):" + (end - start) + "毫秒");
- }
由于默認(rèn)線程池的核心線程數(shù)是8,所以3個(gè)任務(wù)會(huì)同時(shí)開(kāi)始執(zhí)行,日志輸出是這樣的:
- 2021-09-15 00:30:14.819 INFO 77614 --- [ task-2] com.didispace.chapter76.AsyncTasks : 開(kāi)始做任務(wù)二
- 2021-09-15 00:30:14.819 INFO 77614 --- [ task-3] com.didispace.chapter76.AsyncTasks : 開(kāi)始做任務(wù)三
- 2021-09-15 00:30:14.819 INFO 77614 --- [ task-1] com.didispace.chapter76.AsyncTasks : 開(kāi)始做任務(wù)一
- 2021-09-15 00:30:15.491 INFO 77614 --- [ task-2] com.didispace.chapter76.AsyncTasks : 完成任務(wù)二,耗時(shí):672毫秒
- 2021-09-15 00:30:19.496 INFO 77614 --- [ task-3] com.didispace.chapter76.AsyncTasks : 完成任務(wù)三,耗時(shí):4677毫秒
- 2021-09-15 00:30:20.443 INFO 77614 --- [ task-1] com.didispace.chapter76.AsyncTasks : 完成任務(wù)一,耗時(shí):5624毫秒
- 2021-09-15 00:30:20.443 INFO 77614 --- [ main] c.d.chapter76.Chapter76ApplicationTests : 任務(wù)全部完成,總耗時(shí):5653毫秒
接著,可以嘗試在配置文件中增加如下的線程池配置
- 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.thread-name-prefix=task-
日志輸出的順序會(huì)變成如下的順序:
- 2021-09-15 00:31:50.013 INFO 77985 --- [ task-1] com.didispace.chapter76.AsyncTasks : 開(kāi)始做任務(wù)一
- 2021-09-15 00:31:50.013 INFO 77985 --- [ task-2] com.didispace.chapter76.AsyncTasks : 開(kāi)始做任務(wù)二
- 2021-09-15 00:31:52.452 INFO 77985 --- [ task-1] com.didispace.chapter76.AsyncTasks : 完成任務(wù)一,耗時(shí):2439毫秒
- 2021-09-15 00:31:52.452 INFO 77985 --- [ task-1] com.didispace.chapter76.AsyncTasks : 開(kāi)始做任務(wù)三
- 2021-09-15 00:31:55.880 INFO 77985 --- [ task-2] com.didispace.chapter76.AsyncTasks : 完成任務(wù)二,耗時(shí):5867毫秒
- 2021-09-15 00:32:00.346 INFO 77985 --- [ task-1] com.didispace.chapter76.AsyncTasks : 完成任務(wù)三,耗時(shí):7894毫秒
- 2021-09-15 00:32:00.347 INFO 77985 --- [ main] c.d.chapter76.Chapter76ApplicationTests : 任務(wù)全部完成,總耗時(shí):10363毫秒
- 任務(wù)一和任務(wù)二會(huì)馬上占用核心線程,任務(wù)三進(jìn)入隊(duì)列等待
- 任務(wù)一完成,釋放出一個(gè)核心線程,任務(wù)三從隊(duì)列中移出,并占用核心線程開(kāi)始處理
注意:這里可能有的小伙伴會(huì)問(wèn),最大線程不是5么,為什么任務(wù)三是進(jìn)緩沖隊(duì)列,不是創(chuàng)建新線程來(lái)處理嗎?這里要理解緩沖隊(duì)列與最大線程間的關(guān)系:只有在緩沖隊(duì)列滿了之后才會(huì)申請(qǐng)超過(guò)核心線程數(shù)的線程來(lái)進(jìn)行處理。所以,這里只有緩沖隊(duì)列中10個(gè)任務(wù)滿了,再來(lái)第11個(gè)任務(wù)的時(shí)候,才會(huì)在線程池中創(chuàng)建第三個(gè)線程來(lái)處理。這個(gè)這里就不具體寫(xiě)列子了,讀者可以自己調(diào)整下參數(shù),或者調(diào)整下單元測(cè)試來(lái)驗(yàn)證這個(gè)邏輯。