Springboot自定义ThreadPoolTaskExecutor线程池多线程并发执行异步方法

1. 背景

当前因为工作需求,要发送大量Http请求,经过实践遍历发送需要6小时才能发送完毕,如果单线程发送请求会导致主线程阻塞。就会存在以下问题:

  1. 前端用户等待响应时间过长,无法进行下一步操作,不利于用户操作系统
  2. 响应时间过长超过Tomcat服务器会话时间,导致前后端的请求重新连接,这会出现抛出java.io.IOException: 你的主机中的软件中止了一个已建立的连接;重而终止了还未完成的Http发送任务
  3. 如果主线程其他任务如:定时Excel数据批量导入,文件上传等等;很容易因为文件格式问题,导致抛出异常,从而把Http的任务中断
  4. 夜长梦多,长时间发送请求,无法判断是否执行完毕;如果抛出异常,或是需要重启服务,无法判断执行到什么阶段,定位重发位置需要耗费很多时间精力,得不偿失,只能全部重发(一样面临以上问题)。

2. 解决思路

经过考虑可以使用多线程来解决问题,主线程负责开子线程处理请求。其中根据业务需要可以将多线程分为以下2类

  • 任务类型1. 开完子线程就返回响应用户的请求。
  • 任务类型2. 开完子线程后等待子线程响应结果后,再响应用户请求
    在这里插入图片描述

在这里插入图片描述


3. 具体实现

Springboot中已经有集成的ThreadPoolTaskExecutor线程池可以供调用(注意区分Java自带的ThreadPoolExecutor),虽然2种线程池的提供的具体方法不一定一样,但是调度线程过程原理是通用,下面贴出ThreadPoolExecutor的调度线程过程作为创建ThreadPoolTaskExecutor的参考。

在这里插入图片描述

4. 开始编写代码

@EnableAsync 注解启用Spring 异步方法执行功能

当前的线程池 :

  • 核心线程池(CorePool) : 10 ;
  • 最大线程池(MaxPool) : 20 ;
  • 队列长度(QueueCapacity) : 200;
  • 拒绝策略(RejectedExecutionHandler) : CallerRunsPolicy

表示:

  • 如果 请求创建的线程数 <= 10 + 200, 只会使用核心线程池的10个线程执行任务;
  • 如果 10 + 200 < 请求创建的线程数 , 创建额外的10个线线程处理任务, 线程池中同时拥有20个线程处理任务

    使用CallerRunsPolicy策略,主线程还没将 (请求创建的线程数 - 10 + 200 ) 的子线程创建完毕, 主线程处于阻塞状态,需要将所有子线程创建完毕才可以返回响应,即使任务是任务类型1,它的响应时间也会无限接近于 任务类型2;根据业务需求可以通过设置更长的队列长度来解决

    线程处理完任务后,(最大线程池 - 核心线程池) > 0 && 经过60s空闲时间,就会回收除核心线程池外的空闲线程
@EnableAsync //注解启用Spring 异步方法执行功能
@Configuration
public class AsyncTaskConfig {

    @Bean("AsyncTaskThreadPool")
    public Executor threadPoolExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数:线程池创建时候初始化的线程数
        executor.setCorePoolSize(10);
        // 最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(20);
        // 缓冲队列:用来缓冲执行任务的队列
        executor.setQueueCapacity(200);
        // 允许线程的空闲时间60秒:当超过了核心线程之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(60);
        // 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
        executor.setThreadNamePrefix("AsyncTaskThreadPool-");
        // 缓冲队列满了之后的拒绝策略:由调用线程处理(一般是主线程)
        //AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常
        //DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态
        //DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
        //CallerRunsPolicy:不丢弃任务 由调用线程处理该任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
        executor.setAllowCoreThreadTimeOut(true);
        executor.initialize();
        return executor;
    }

}


@Async 根据添加到自定义线程池中执行

@Component
public class AsyncRequestComponent {

    @Async("AsyncTaskThreadPool")
    public CompletableFuture<String> mockHttpRequest(String url) throws InterruptedException, IOException {
        String threadName = Thread.currentThread().getName();
        System.out.println("线程" + threadName + "开始调用,输出:" + url);
        String result = "";
        // HttpClient RestTemplate Feign or 其他Http请求框架
        // 或者其他需要 异步并发 的 逻辑代码 
        // Thread.sleep(50);
        result = HttpClientUtil.get(url);

        System.out.println("线程" + threadName + "调用结束,输出:" + url);
        return CompletableFuture.completedFuture(result);
    }
}


@Slf4j
@SpringBootTest
class DemoTestApplicationTests {
    @Autowired
    private AsyncRequestComponent asyncRequestComponent;

     @Test
    void testAsyncThreadPerformance() throws InterruptedException, ExecutionException {

        long start = System.currentTimeMillis();
        List<CompletableFuture<String>> list = new ArrayList<>();
        // 通过修改循环次数来查看 任务数<缓存队列+核心线程 和 任务数>缓存队列+核心线程 的主线程花费时间
        // 如果 任务数<缓存队列+核心线程 主线程不需要等待
        // 如果 任务数>缓存队列+核心线程 主线程需要等待 因为后面任务还没创建成功
        for(int i=0 ;i < 2000; i++){
            CompletableFuture<String> future= asyncRequestComponent.mockHttpRequest(i);
            list.add(future);
        }

        // 任务类型1 主线程不需要等待所有子线程执行完毕,将下面的for循环注释
        // 任务类型2 主线程需要等待所有子线程执行完毕:需要遍历future执行get方法等待子线程执行完毕
        for(Future<?> future : list){
            while (true) {//CPU高速轮询:每个future都并发轮循,判断完成状态然后获取结果,这一行,是本实现方案的精髓所在。即有10个future在高速轮询,完成一个future的获取结果,就关闭一个轮询
                if (future.isDone() && !future.isCancelled()) { //获取future成功完成状态,如果想要限制每个任务的超时时间,取消本行的状态判断+future.get(1000*1, TimeUnit.MILLISECONDS)+catch超时异常使用即可。
                    String result = (String) future.get();//获取结果
                    System.out.println("任务i=" + result + "获取完成!" + new Date());
                    break;//当前future获取结果完毕,跳出while
                } else {
                    Thread.sleep(1);//每次轮询休息1毫秒(CPU纳秒级),避免CPU高速轮循耗空CPU---》新手别忘记这个
                }
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("主线程花费时间:"+(end-start));
    }

}


5. 结论

  • 如果希望主线程永远不会阻塞,不设置队列长度,因为默认长度设置为Integer.MAX_VALUE; 因为队列长度很长,不会额外再创建线程处理了,所以核心线程(CorePool) = 最大线程(MaxPool)
  • 如果本身业务用户可以容忍一定范围的响应延迟,这就需要根据实际出发,统计子线程任务的平均执行实际,判断核心线程数、最大线程数、缓冲队列的容量



题外话:如何创建线程池

CPU密集型:对于 CPU 密集型计算,多线程本质上是提升多核 CPU 的利用率,所以对于一个 8 核的 CPU,每个核一个线程,理论上创建 8 个线程就就可以;理论公式:最大线程数 = CPU核数 + 1

IO 密集型任务: IO 密集型任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。理论公式 : 最大线程数 = 最大线程数 = CPU核心数 / (1 - IO_TIME/REQUEST_TIME) ,比如在某个请求中,请求时长为10秒,调用IO时间为8秒,这时我们阻塞占百分比就是80%,有效利用CPU占比就是20%,假设是八核CPU,我们线程数就是8 / (1 - 80%) = 8 / 0.2 = 40个

最大线程数:

  • CPU密集型:CPU 核心数 + 1
  • IO密集型:CPU 核心数 / (1 - IO_TIME/REQUEST_TIME)

核心线程数: 核心线程数 = 最大线程数 * 20%


获取CPU核心数命令:

window - Java : Runtime.getRuntime().availableProcessors()//获取逻辑核心数,如6核心12线程,那么返回的是12

linux - 物理CPU: cat /proc/cpuinfo| grep “physical id”| sort| uniq| wc -l

linux - 物理CPU中core: cat /proc/cpuinfo| grep “cpu cores”| uniq

linux - 逻辑CPU: cat /proc/cpuinfo| grep “processor”| wc -l


偷懒大法:

  • 核心线程数:CPU核心数
  • 最大线程数:CPU逻辑CPU( CPU核心数 * 2)

参考资料:

https://blog.youkuaiyun.com/weixin_49258262/article/details/125463819?spm=1001.2014.3001.5506
https://blog.youkuaiyun.com/Wei_Naijia/article/details/127028398?spm=1001.2014.3001.5506
https://blog.youkuaiyun.com/wozyb1995/article/details/125044992?spm=1001.2014.3001.5506
https://blog.youkuaiyun.com/iampatrick_star/article/details/124490586?spm=1001.2014.3001.5506
https://www.cnblogs.com/651434092qq/p/14240406.html
https://juejin.cn/post/6948034657321484318
https://juejin.cn/post/7072281409053786120

<think>我们正在讨论在Spring Boot应用中启动200个线程并发执行任务。根据提供的引用,我们可以使用两种主要方法:继承Thread或实现Runnable接口,以及使用@Async注解。同时,引用中还提到了动态多线程并发定时任务的实现和线程池的配置。由于我们需要同时启动200个线程,使用线程池是更合理的选择,因为直接创建200个线程可能会导致资源耗尽和性能下降。 步骤: 1. 配置线程池:在Spring Boot中,我们可以通过配置一个TaskExecutor来定义线程池的参数,包括核心线程数、最大线程数、队列容量等。 2. 使用@Async注解:在需要并发执行方法上添加@Async注解,这样该方法就会在配置的线程池执行。 具体配置方法: 首先,我们需要创建一个线程池配置类。根据引用[3],我们可以设置线程池关闭时等待任务完成,以确保优雅关闭。 然后,在需要并发执行方法上使用@Async注解。 但是,注意引用[4]中提到了在启动类上添加@EnableScheduling和@EnableAsync(虽然引用中没有直接提到@EnableAsync,但使用@Async需要它)来启用异步任务执行。 另外,引用[2]展示了动态调用任务配置信息,但我们的需求是同时启动200个线程,我们可以通过循环调用异步方法200次来实现。 配置线程池示例: 创建一个配置类,例如: ```java 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.Executor; @Configuration @EnableAsync public class AsyncConfig { @Bean(name = "taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 设置核心线程数,根据需求调整 executor.setCorePoolSize(10); // 设置最大线程数,这里我们设置为200,因为最多需要同时运行200个线程 executor.setMaxPoolSize(200); // 设置队列容量,如果任务数超过核心线程数,多余的任务会进入队列等待 executor.setQueueCapacity(500); executor.setThreadNamePrefix("AsyncThread-"); executor.initialize(); // 设置线程池关闭时等待所有任务完成 executor.setWaitForTasksToCompleteOnShutdown(true); return executor; } } ``` 然后,在服务类中,我们可以使用@Async注解来标记异步方法: ```java import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class AsyncService { @Async("taskExecutor") // 指定使用上面配置的taskExecutor public void executeTask(int taskNumber) { // 执行具体的任务,taskNumber可以表示任务编号 System.out.println("Executing task " + taskNumber + " in thread: " + Thread.currentThread().getName()); // 这里写具体的业务逻辑 } } ``` 最后,在需要启动200个线程的地方,注入AsyncService并循环调用200次: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class TaskRunner { @Autowired private AsyncService asyncService; public void runTasks() { for (int i = 0; i < 200; i++) { asyncService.executeTask(i); } } } ``` 注意: 1. 线程池的配置参数需要根据实际需求和系统资源进行调整。上面配置中,核心线程数为10,最大线程数为200,队列容量为500。这意味着当同时提交的任务数超过核心线程数(10)时,任务会进入队列,当队列满了(500个任务)后,会继续创建新线程直到达到最大线程数(200)。如果超过200个线程且队列已满,将根据拒绝策略处理(默认是AbortPolicy,会抛出异常)。 2. 由于我们一次提交200个任务,所以这些任务会先进入队列,然后由线程池中的线程逐步处理。但注意,我们配置的最大线程数是200,所以理论上可以同时运行200个任务(如果队列中的任务被取出并执行)。但是,实际上线程池并不会一开始就创建200个线程,而是根据任务提交情况逐步增加到最大线程数。 另外,引用[3]中提到了设置等待任务完成,即`executor.setWaitForTasksToCompleteOnShutdown(true);`,这确保了在应用关闭时,线程池会等待所有任务完成后再销毁。 关于动态任务,引用[2]和[4]主要涉及定时任务,但我们的需求是一次性启动200个线程,所以上述方法即可满足。 但是,如果这200个任务都是定时任务,那么我们需要使用动态定时任务的方式。根据引用[4],我们可以在启动类上添加@EnableScheduling,然后使用ScheduledAnnotationBeanPostProcessor来动态注册定时任务。不过,用户的问题并没有提到定时任务,所以我们按照一次性并发执行来处理。 如果用户需要定时并发执行,那么可以结合@Scheduled注解和动态任务注册,但这里我们只关注并发执行。 因此,按照上述配置,我们可以在Spring Boot应用中启动200个线程并发执行任务。 注意:在实际生产环境中,启动200个线程需要谨慎,确保服务器资源(如内存和CPU)能够支持。 相关问题: 1. 如何配置Spring Boot线程池以优化大量并发任务? 2. 使用@Async注解时,如何捕获并处理异常? 3. 在Spring Boot中如何动态调整线程池参数? 4. 如何监控Spring Boot线程池的运行状态? 5. 除了使用@Async,还有哪些方式可以在Spring Boot中实现多线程并发?</think>### Spring Boot 启动 200 个线程并发执行任务配置方法Spring Boot 中配置 200 个线程并发执行任务,需结合线程池异步任务机制实现。以下是完整配置步骤: --- #### 1. **线程池配置类** 创建线程池配置类,设置核心参数: ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration @EnableAsync // 启用异步支持 public class ThreadPoolConfig { @Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程数(根据服务器CPU核数调整) executor.setCorePoolSize(50); // 最大线程数(需支持200并发) executor.setMaxPoolSize(200); // 任务队列容量(缓冲超额任务) executor.setQueueCapacity(1000); // 线程名前缀 executor.setThreadNamePrefix("ConcurrentThread-"); // 优雅停机:等待所有任务完成再关闭 executor.setWaitForTasksToCompleteOnShutdown(true); // [^3] // 拒绝策略:队列满时由调用线程处理 executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy() ); executor.initialize(); return executor; } } ``` --- #### 2. **异步任务服务** 使用 `@Async` 注解标记异步方法: ```java import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class TaskService { // 指定使用配置的线程池 @Async("taskExecutor") public void executeTask(int taskId) { // 具体任务逻辑(如数据库操作、API调用等) System.out.println("执行任务: " + taskId + " | 线程: " + Thread.currentThread().getName()); } } ``` --- #### 3. **启动并发任务** 在控制器或启动类中触发 200 个任务: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TaskController { @Autowired private TaskService taskService; @GetMapping("/startTasks") public String startConcurrentTasks() { for (int i = 0; i < 200; i++) { taskService.executeTask(i); // 异步触发任务 } return "已启动200个并发任务"; } } ``` --- #### 4. **关键配置说明** | 参数 | 值 | 作用 | |--------------------|-------|----------------------------------------------------------------------| | `corePoolSize` | 50 | 常驻线程数(避免频繁创建销毁) | | `maxPoolSize` | 200 | **支持200并发**的关键设置 | | `queueCapacity` | 1000 | 缓冲队列大小(防止瞬时流量冲击) | | `WaitForTasksToCompleteOnShutdown` | true | 确保停机时任务不丢失 [^3] | --- #### 5. **注意事项** 1. **资源评估**:200 线程并发需确保服务器资源充足(建议 4 核 8G 以上配置) 2. **连接池调整**:若涉及数据库,需同步调大连接池(如 HikariCP 的 `maximumPoolSize`) 3. **监控建议**:通过 Spring Boot Actuator 监控线程池状态(如活跃线程数、队列大小) 4. **动态调整**:参考动态线程池方案实现运行时参数修改 [^2] > **关键原理**:通过 `@Async` 将任务提交到线程池,由线程池根据 `maxPoolSize` 创建最多 200 个线程并行处理任务,超额任务进入队列缓冲 [^1][^4]。 --- ### 相关问题 1. **如何避免线程池任务被 Spring Boot 停机时中断?** > 配置 `setWaitForTasksToCompleteOnShutdown(true)` 保证任务完成 [^3] 2. **线程池队列满了会导致什么后果?如何优化?** > 会触发拒绝策略,默认抛出异常。可通过增大队列或实现自定义拒绝策略处理。 3. **如何实现 Spring Boot 动态调整线程池参数?** > 需结合配置中心(如 Nacos)和监听器动态刷新参数 [^2]。 4. **高并发下如何保证线程安全?** > 需使用线程安全组件(如 ConcurrentHashMap)、同步锁或原子类。 5. **除了 `@Async`,还有哪些实现并发的方案?** > 可使用 `CompletableFuture` 或 Reactor 响应式编程实现更精细控制。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值