springboot中的线程操作

文章目录

概述

在 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 或日志中定位问题。

相关推荐
敲代码的嘎仔1 小时前
自己设计了一个兑换码算法:自增ID + Base32转码 + 按位加权签名 + 异或混淆,面试被追问细节时终于不用慌了
java·数据库·mysql·算法·微服务·面试·职场和发展
Sinclair1 小时前
MCP 功能详解:内置 108 个工具,让 AI 直接帮你管理网站
后端·mcp
用户233376852181 小时前
接口卡死排查实录-缺失return的UB死循环
前端·后端
用户489148799762 小时前
Go 系统服务开发实战:systemd+sdnotify + 看门狗-agent与系统服务
后端
拾光师2 小时前
Python 模式匹配:从 in 到正则再到 match-case,一招对一招
后端
m0_587383002 小时前
外卖CPS系统开发实战:从架构设计到运营落地全指南
java·spring·小程序·架构·需求分析
zhangzeyuaaa2 小时前
Python asyncio 事件循环演进:从手动管理到现代化实践
java·服务器·python
只爱喝胡辣汤2 小时前
JUC 并发工具与线程安全源码深度解析
后端
只爱喝胡辣汤2 小时前
03-KafkaProducer 源码分析
后端