文章目录
- 概述
- 核心使用场景与技术解析
-
- [Web 请求处理线程 (Tomcat/Undertow Worker Threads)](#Web 请求处理线程 (Tomcat/Undertow Worker Threads))
- 异步任务处理线程 (@Async)
- 定时任务调度线程 (@Scheduled)
- 消息队列消费线程 (Kafka / RabbitMQ Listener)
- 数据库连接池工作线程 (HikariCP)
- [Redis 客户端工作线程 (Lettuce / Redisson)](#Redis 客户端工作线程 (Lettuce / Redisson))
- 自定义并发处理线程 (CompletableFuture / ThreadPoolExecutor)
概述
在 Spring Boot 项目中,严禁直接使用 new Thread() 创建裸线程。所有线程的创建与管理都应交由框架或显式配置的线程池(Thread Pool)来接管,以实现资源复用、避免 OOM(内存溢出),并便于统一的监控与调优。
Spring Boot 中的线程主要分为两大类:
框架托管线程:Web 容器线程、定时任务线程、消息监听线程等。
业务自定义线程:@Async 异步任务、CompletableFuture 并发编排、自定义 ThreadPoolExecutor。
核心使用场景与技术解析
Web 请求处理线程 (Tomcat/Undertow Worker Threads)
场景描述:处理外部 HTTP/HTTPS 请求,执行 Controller、Service 逻辑并返回响应。
技术原理:
以默认的 Tomcat 为例,采用 NIO 模型(非阻塞式IO)。包含 Acceptor 线程(接收连接)、Poller 线程(监听就绪事件)和 Worker 线程池(实际处理业务逻辑)。
默认配置下,Tomcat 的最大工作线程数 (server.tomcat.threads.max) 为 200。当 200 个线程都在处理请求时,新请求会进入队列 (server.tomcat.threads.max-queue-capacity,默认 100),队列满后拒绝请求。
代码/配置示例:
yaml
server:
tomcat:
threads:
max: 200
min-spare: 10
max-connections: 8192
accept-count: 100
最佳实践与避坑:
避坑:绝对不要在 Controller 中执行耗时操作(如大文件下载、复杂报表生成、长轮询),这会迅速耗尽 Worker 线程,导致整个应用拒绝服务(DOS)。耗时操作应移交异步线程池或消息队列。
规范:为线程命名,便于日志追踪(Tomcat 默认命名为 http-nio-8080-exec-x)。
异步任务处理线程 (@Async)
场景描述:非核心链路的解耦操作,如:发送通知邮件/短信、记录操作审计日志、异步更新缓存。
技术原理:Spring 通过 AOP 拦截带有 @Async 注解的方法,将其提交到 TaskExecutor(线程池)中异步执行,调用方线程立即返回,不阻塞主流程。
示例代码:
java
@Service
public class NotificationService {
// 必须指定自定义的线程池名称,否则使用默认的 SimpleAsyncTaskExecutor
@Async("asyncTaskExecutor")
public void sendEmailAsync(String email, String content) {
// 异步发送邮件逻辑
}
}
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("asyncTaskExecutor")
public Executor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("Async-Email-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
最佳实践与避坑:
致命陷阱:如果不指定自定义线程池,Spring 默认使用 SimpleAsyncTaskExecutor,它每次调用都会 new Thread(),高并发下会瞬间导致 OutOfMemoryError: unable to create new native thread。
事务失效:@Async 方法在新线程中执行,无法共享调用方线程的数据库事务 (@Transactional)。如果异步方法内需要事务,必须在异步方法内部重新标注 @Transactional。
同类调用失效:在同一个类中,非异步方法直接调用异步方法,@Async 会失效(因为绕过了 Spring AOP 代理)。应通过注入自身 Bean 或 AopContext.currentProxy() 调用。
定时任务调度线程 (@Scheduled)
场景描述:周期性执行的任务,如:每日凌晨对账、定时清理过期缓存、定时同步第三方数据。
技术原理:Spring 默认提供一个单线程的 ThreadPoolTaskScheduler 来执行所有 @Scheduled 任务。
最佳实践与避坑:
致命陷阱:由于默认是单线程,如果任务 A 执行耗时 10 秒,任务 B 即使到了触发时间,也必须等待任务 A 执行完毕,导致任务堆积和延迟。
解决方案:必须自定义调度线程池,增加并发度。
示例代码:
java
@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 根据定时任务数量设置
scheduler.setThreadNamePrefix("Scheduled-Task-");
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}
消息队列消费线程 (Kafka / RabbitMQ Listener)
场景描述:异步解耦、削峰填谷。监听 MQ 消息并执行业务逻辑。
技术原理:MQ 客户端(如 Spring Kafka)内部维护了监听器容器(Listener Container),为每个 Topic/Partition 分配独立的消费线程。
示例代码:
ymal
spring:
kafka:
listener:
concurrency: 3 # 消费线程数,建议等于或小于 Partition 数量
最佳实践与避坑:
消费线程数 (concurrency) 不应大于 Topic 的 Partition 数量,否则多余的线程将处于空闲状态。
消费逻辑中若包含数据库操作,需注意幂等性设计,因为 MQ 可能会重复投递消息。
数据库连接池工作线程 (HikariCP)
场景描述:执行 SQL 查询与更新。
技术原理解析(常见误区澄清):HikariCP 本身不创建工作线程去执行 SQL!
当 Web 线程或异步线程需要操作数据库时,它会向 HikariCP 申请一个 Connection。获取到 Connection 后,依然是由当前的业务线程(如 Tomcat Worker 线程)去执行 JDBC 调用。HikariCP 只是一个高性能的"连接对象池"。
最佳实践:
线程池大小 (maximum-pool-size) 并非越大越好。对于 CPU 密集型应用,公式约为:核心数 * 2 + 有效磁盘数;对于 IO 密集型(大多数 Web 应用),可设置为 核心数 * (1 + 平均等待时间/平均计算时间),通常 20-50 即可满足绝大多数场景。
Redis 客户端工作线程 (Lettuce / Redisson)
场景描述:缓存读写、分布式锁、发布订阅。
技术原理:
Lettuce (Spring Boot 默认):基于 Netty 构建,采用 EventLoop (事件循环) 线程模型。默认情况下,所有 Redis 命令的发送和响应解析都由少数几个 Netty EventLoop 线程处理(通常是 CPU 核心数相关)。
Redisson:同样基于 Netty,但提供了更丰富的分布式数据结构,其底层也是异步非阻塞的 EventLoop 模型。
最佳实践与避坑(极度重要):
绝对禁止在 Redis 回调或 Lua 脚本执行中做阻塞操作。如果你在 Lettuce 的异步回调中执行了 Thread.sleep() 或耗时 DB 查询,会直接阻塞 Netty EventLoop 线程,导致整个应用的 Redis 请求全部卡死(表现为 Redis 假死,但 CPU 可能不高)。
如果使用 Redisson 的 RLock,确保在 finally 块中释放锁,且释放锁的逻辑必须在获取锁的同一个线程中执行(Redisson 3.15+ 支持看门狗机制,但仍需注意线程归属)。
自定义并发处理线程 (CompletableFuture / ThreadPoolExecutor)
场景描述:需要并行执行多个独立任务以缩短总响应时间(如:并行查询用户信息、订单信息、积分信息,然后聚合返回)。
技术原理:利用 Java 8+ 的 CompletableFuture 结合自定义线程池,实现任务的编排(allOf, anyOf)、异常处理和结果聚合。
代码示例(结合您之前的问题优化):
java
@Service
public class DashboardService {
// 注入专用的业务线程池,避免与 @Async 或其他任务混用
@Resource(name = "customBusinessExecutor")
private Executor customExecutor;
public DashboardVO getDashboard(Long userId) {
CompletableFuture<UserInfo> userFuture = CompletableFuture.supplyAsync(
() -> userService.getUserInfo(userId), customExecutor);
CompletableFuture<OrderInfo> orderFuture = CompletableFuture.supplyAsync(
() -> orderService.getOrderInfo(userId), customExecutor);
// 使用 handle 统一处理异常,避免异常丢失或包装成 CompletionException
return CompletableFuture.allOf(userFuture, orderFuture)
.handle((result, ex) -> {
if (ex != null) {
log.error("聚合查询失败, userId={}", userId, ex);
throw new BusinessException("数据加载失败", ex);
}
return new DashboardVO(userFuture.join(), orderFuture.join());
}).join(); // 阻塞当前 Web 线程等待结果
}
}
最佳实践:
必须显式传入自定义 Executor。如果不传,supplyAsync 默认使用 ForkJoinPool.commonPool(),该线程池所有任务共享,一旦某个任务阻塞,会影响全局(包括 JVM 内部的并行流操作)。
使用 NamedThreadFactory 为线程命名,如 Dashboard-Query-Thread-,便于在 Arthas 或日志中定位问题。