springboot自帶線程池ThreadPoolTaskExecutor使用
不管是阿里,還是華為java開發(fā)手冊,都會有一條建議,就是讓開發(fā)者不要使用Executors去創(chuàng)建線程池,而是使用構造函數ThreadPoolExecutor的方式來創(chuàng)建,并設置合理的參數。原因如下:
說明:Executors 返回的線程池對象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2) CachedThreadPool:
允許的創(chuàng)建線程數量為 Integer.MAX_VALUE,可能會創(chuàng)建大量的線程,從而導致 OOM。
在spring框架中,spring提供了ThreadPoolTaskExecutor來創(chuàng)建線程池,該類在spring-context包下。其實ThreadPoolTaskExecutor是對ThreadPoolExecutor的封裝。
到了springboot這里,因為引入了spring-boot-autoconfigurer,自動裝配機制,在task包下,直接把ThreadPoolTaskExecutor注入到bean容器里面。所以在springboot項目中,如果要使用線程池,可以直接使用,都不用額外任何配置。
springboot自動裝配的線程池使用的配置如下:

默認核心線程數是8個。最大線程數和等待隊列都是Integer.MAX_VALUE。綜合上面的介紹,默認配置的線程池其實也有OOM的風險。
這里使用的springboot版本是2.7.8。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>springexample</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.8</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>異步任務類:在方法上添加@Async注解,可以讓他啟用線程池處理異步任務。
/*
* xxx co.ltd Copyright @ 2023-2023 All Rights Reserved
*/
package com.example.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
/**
* 描述信息
*
* @author Administrator
* @since 2023/4/2 7:30
*/
@Slf4j
@Component
public class AsyncTask {
private Random random = new Random();
@Async
public CompletableFuture<String> doTaskOne() throws Exception {
log.info("task one start.");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("task one done,cost " + (end - start) + "ms.");
return CompletableFuture.completedFuture("task one done");
}
@Async
public CompletableFuture<String> doTaskTwo() throws Exception {
log.info("task two start.");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("task two done,cost " + (end - start) + "ms.");
return CompletableFuture.completedFuture("task two done");
}
@Async
public CompletableFuture<String> doTaskThree() throws Exception {
log.info("task three start.");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("task three done,cost " + (end - start) + "ms.");
return CompletableFuture.completedFuture("task three done");
}
}啟動類:啟動類上添加注解@EnableAsync開啟異步
/*
* xxx co.ltd Copyright @ 2023-2023 All Rights Reserved
*/
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* 描述信息
*
* @author Administrator
* @since 2023/4/2 7:29
*/
@SpringBootApplication
@EnableAsync
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}測試類:
/*
* xxx co.ltd Copyright @ 2023-2023 All Rights Reserved
*/
package com.example.task;
import com.example.App;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.CompletableFuture;
/**
* 描述信息
*
* @author Administrator
* @since 2023/4/2 7:34
*/
@SpringBootTest(classes = {App.class})
@Slf4j
public class AsyncTaskTest {
@Autowired
private AsyncTask asyncTask;
@Test
public void testTask() throws Exception {
long start = System.currentTimeMillis();
CompletableFuture<String> taskOne = asyncTask.doTaskOne();
CompletableFuture<String> taskTwo = asyncTask.doTaskTwo();
CompletableFuture<String> taskThree = asyncTask.doTaskThree();
CompletableFuture.allOf(taskOne, taskTwo, taskThree).join();
long end = System.currentTimeMillis();
log.info("all task done,cost " + (end - start) + " ms");
}
}這里設置了3個任務,默認線程池核心線程數是8個, 所以這3個任務在線程池環(huán)境中,基本都是同時運行,所以總體運行時間肯定會大于他們3各種最耗時的一個任務,小于三個任務耗時之和。
運行這個測試用例,打印結果:

從打印結果來看,3個任務幾乎同時執(zhí)行,運行結束,分別耗時:2094ms、 653ms、 1505ms,最后總耗時2129ms,符合預期。
這里打印的線程池前綴是task-,也是默認線程池配置。
在springboot配置中,提供了可以配置線程池的參數:
spring:
task:
execution:
pool:
core-size: 2
max-size: 5
queue-capacity: 10
thread-name-prefix: test-task-這些參數都不是我們自定義的,而是springboot配置文件中指定的參數名。所以我們可以通過yml自動提示類進行配置:

這里也可以看出默認線程池配置核心數量是8個, 這里我們設置為2,來驗證線程池工作原理。
這里有3個任務,核心線程數是2,所以只能先執(zhí)行2個任務,剩下的進入隊列等待,當前面一個任務執(zhí)行完成,最后一個任務才會從等待隊列中進入核心線程進行執(zhí)行,重新運行單元測試,打印信息如下:

這個打印結果,剛開始任務1,2都運行,任務2完成之后,任務3開始執(zhí)行。
因為修改了線程前綴,這里打印的線程前綴是test-task-,從線程前綴 + 線程數上來看,這里最大線程數是2,因為前面設置的核心線程數就是2。
相信做過springboot線程池相關的測試,可能有的人得出的結論和我這里不太一樣,springboot默認線程池是SimpleAsyncTaskExecutor。這個原因呢,有兩個,一個是springboot版本的原因,默認不做任何配置,一樣的代碼,上面運行打印的線程池前綴就是SimpleAsyncTaskExecutor。另外一個原因就是上面提到的ThreadPoolTaskExecutor在配置的時候,其實使用了一個特別的注解:@ConditionalOnMissingBean({Executor.class}),如下所示:

這個注解的意思是,當bean容器中沒有Executor.class這個實例的時候,進行配置。也即是說其他地方配置了線程器Executor,那么這個ThreadPoolTaskExecutor的bean就不會被配置。這也就是大家的結論里面spring線程池默認不是ThreadPoolTaskExecutor的原因。
我這里通過引入spring-boot-starter-websocket依賴,然后配置websocket:
/*
* xxx co.ltd Copyright @ 2023-2023 All Rights Reserved
*/
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* 描述信息
*
* @author Administrator
* @since 2023/4/2 20:14
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/tmax/ws").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/user","/topic");
registry.setApplicationDestinationPrefixes("/app");
registry.setUserDestinationPrefix("/user");
}
}同樣的,我們再次測試,發(fā)現結果如下所示:

這里也驗證了上面@ConditionalOnMissingBean({Executor.class})注解的作用,因為有了別的線程池,所以這里ThreadPoolTaskExecutor線程池就沒有被加載。這里的線程池就是SimpleAsyncTaskExecutor。這個線程池其實不是一個真正的線程池,因為它每次都會創(chuàng)建新線程,這個線程池創(chuàng)建的目的其實就是為了執(zhí)行少量短時間的任務,并不適合在高并發(fā)場景下。
通過上面的實驗,我們知道,在springboot 2.7.8版本里面,如果沒有其他配置,默認線程池就是ThreadPoolTaskExecutor,而且可以不用任何配置就可以使用。但是它還是有OOM的風險,因為它的max-size和queue-capacity都是Integer.MAX_VALUE,所以我們需要修改它的默認線程池配置信息。但是默認線程池有個德性,就是如果配置了其他線程池,它又不會被加載。
所以一般的項目里面,我們都是進行如下所示的手動配置:
/*
* xxx co.ltd Copyright @ 2023-2023 All Rights Reserved
*/
package com.example.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 描述信息
*
* @author Administrator
* @since 2023/4/2 20:36
*/
@Configuration
@EnableAsync
public class ThreadPoolConfig {
@Value("${spring.task.execution.pool.core-size}")
private int corePoolSize;
@Value("${spring.task.execution.pool.max-size}")
private int maxPoolSize;
@Value("${spring.task.execution.pool.queue-capacity}")
private int queueCapacity;
@Value("${spring.task.execution.thread-name-prefix}")
private String threadNamePrefix;
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setThreadNamePrefix(threadNamePrefix);
// 拒絕策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}另外,一定要結合application.yml中設置的線程池配置信息使用,這樣才符合文章開頭所說的大廠java開發(fā)手冊中建議使用自定義參數配置線程池,避免OOM風險。
到此這篇關于springboot自帶線程池ThreadPoolTaskExecutor使用的文章就介紹到這了,更多相關springboot ThreadPoolTaskExecutor 內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java存儲過程調用CallableStatement的方法
這篇文章主要介紹了Java存儲過程調用CallableStatement的方法,幫助大家更好的理解和學習Java,感興趣的朋友可以了解下2020-11-11
Java?詳解Collection集合之ArrayList和HashSet
本章具體介紹了ArrayList和HashSet兩種集合的基本使用方法和區(qū)別,圖解穿插代碼實現。?JAVA成仙路從基礎開始講,后續(xù)會講到JAVA高級,中間會穿插面試題和項目實戰(zhàn),希望能給大家?guī)韼椭?/div> 2022-03-03
Java項目Guava包?HashMultimap使用及注意事項
guava基本上可以說是java開發(fā)項目中,大概率會引入的包,今天介紹的主角是一個特殊的容器HashMultmap,可以簡單的將它的數據結構理解為Map<K,?Set<V>>,今天主要介紹下基礎的知識點?HashMultmap級使用,感興趣的朋友一起看看吧2022-05-05最新評論

