SpringBoot 整合 ThreadPoolTaskExecutor 自定义线程池实战
前言
在后端开发里,异步处理是非常常见的场景:发送短信、消息推送、日志归档、非实时统计、异步业务解耦。很多新手图省事,直接new Thread()或者@Async裸用,一旦并发量上来,不断创建线程会抢占CPU、内存资源,严重时引发服务宕机。
JDK自带ThreadPoolExecutor线程池,Spring对它做了封装,提供ThreadPoolTaskExecutor,适配Spring上下文、支持@Async注解,也是企业项目里最常用的方案。本文从零搭建,从配置、代码、测试、踩坑四个部分完整落地。
一、核心参数回顾
配置线程池,最重要就是这几个参数,先简单理清含义:
| 参数 | 说明 |
|---|---|
| corePoolSize | 核心线程数,线程池长期保留的线程,不会被回收 |
| maximumPoolSize | 最大线程数,线程池允许创建线程上限 |
| keepAliveSeconds | 非核心线程空闲超时时间,超时后回收 |
| queueCapacity | 阻塞队列容量,核心线程满了之后,任务先放进队列 |
| rejectedExecutionHandler | 拒绝策略,队列和线程都满了,新任务怎么处理 |
执行顺序:来了任务 → 核心线程未满,新建核心线程;核心线程满 → 任务入阻塞队列;队列满 → 创建非核心线程;线程总数达到最大值 → 触发拒绝策略。
拒绝策略内置四种:
AbortPolicy:直接抛异常(默认)CallerRunsPolicy:交给调用者线程执行,不丢弃任务DiscardPolicy:默默丢弃新来任务,无异常DiscardOldestPolicy:丢弃队列里最老未执行任务,放入新任务
生产环境优先选择CallerRunsPolicy,避免任务无声丢失。
二、Maven依赖
基础SpringBoot Web依赖即可,不需要额外包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
三、编写线程池配置类
通过@Configuration + @Bean注册线程池,开启异步注解@EnableAsync:
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;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
@Bean("businessTaskExecutor")
public Executor businessTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(4);
// 最大线程数
executor.setMaxPoolSize(8);
// 队列容量
executor.setQueueCapacity(200);
// 空闲线程超时时间
executor.setKeepAliveSeconds(60);
// 线程名称前缀,方便日志排查
executor.setThreadNamePrefix("business-async-");
// 拒绝策略:由调用线程处理
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务完成后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 关闭等待超时时间
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}
四、异步Service业务代码
使用@Async("bean名称")指定使用我们自定义的线程池,不使用默认池:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class AsyncBusinessService {
@Async("businessTaskExecutor")
public void sendSmsAsync(String phone, String content) {
log.info("开始发送短信,手机号:{},当前线程:{}", phone, Thread.currentThread().getName());
try {
// 模拟调用短信接口耗时
Thread.sleep(800);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("短信发送异常", e);
}
log.info("短信发送完成,手机号:{}", phone);
}
@Async("businessTaskExecutor")
public void saveOperateLogAsync(String userId, String operate) {
log.info("异步保存操作日志,用户:{},操作:{},线程:{}", userId, operate, Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("保存日志异常", e);
}
log.info("操作日志保存成功");
}
}
五、测试Controller
写一个接口批量触发异步任务测试效果:
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/async")
@RequiredArgsConstructor
public class AsyncTestController {
private final AsyncBusinessService asyncBusinessService;
@GetMapping("/test")
public String testAsync() {
for (int i = 1; i <= 10; i++) {
asyncBusinessService.sendSmsAsync("1380000000" + i, "验证码:" + 1000 + i);
asyncBusinessService.saveOperateLogAsync("user" + i, "查询订单");
}
return "任务提交成功,后台异步执行";
}
}
六、常见坑点讲解
- 同一个类里面调用@Async方法,异步失效
原因:Spring AOP代理机制,内部调用不走代理对象,注解不会生效。解决方案:把异步方法抽离到其他Service,或者自己注入自己。 - 默认线程池
SimpleAsyncTaskExecutor不是真线程池,每次新建线程
线上高并发下绝对不能裸用@Async不指定线程池。 - 核心线程数设置经验
- IO密集型(短信、HTTP、DB查询):核心线程数设置为 CPU核心数 * 2 ~ 4
- CPU密集型(计算、加密):核心线程数≈CPU核心数,避免上下文切换开销
- 队列不要设置无限大
queueCapacity如果设置Integer.MAX_VALUE,任务会无限堆积,内存持续上涨最终OOM。
七、生产环境监控扩展
可以通过ThreadPoolTaskExecutor提供的方法拿到运行指标,接入监控:
// 获取线程池运行状态
int activeCount = executor.getActiveCount();
int poolSize = executor.getPoolSize();
long completedTaskCount = executor.getCompletedTaskCount();
可以把指标上报Prometheus,配合告警,当队列堆积数量持续上涨时及时告警。
八、总结
自定义ThreadPoolTaskExecutor是SpringBoot异步开发的标准方案,相比JDK原生ThreadPoolExecutor,它更好融入Spring容器,配合@Async非常简洁。
开发中重点把控:合理设置核心线程、队列长度、拒绝策略;做好线程命名方便日志排查;规避同类内调用异步失效、队列无限大等经典坑点。异步只是手段,不要滥用,异步链路异常、重试、补偿机制都需要业务额外考虑。
要不要我再换下一个主题,比如 SpringBoot整合Redisson分布式锁 这一篇?
- 点赞
- 收藏
- 关注作者
评论(0)