前言

线程池打满会导致接口慢、任务不执行、队列堆积甚至内存溢出。面试中常问:线程池参数怎么配?队列满了怎么办?为什么不能所有业务共用一个线程池?

典型现象

  • 异步任务延迟执行。
  • 接口卡在 Future.get。
  • 日志出现 RejectedExecutionException。
  • 队列长度持续增长。
  • CPU 不高但请求很慢。
  • 某个慢任务拖垮所有异步任务。

线程池执行模型

flowchart LR
    A[提交任务] --> B{核心线程是否满}
    B -->|否| C[创建核心线程]
    B -->|是| D{队列是否满}
    D -->|否| E[进入队列]
    D -->|是| F{最大线程是否满}
    F -->|否| G[创建非核心线程]
    F -->|是| H[执行拒绝策略]

排查指标

线程池必须监控:

  • corePoolSize。
  • maximumPoolSize。
  • activeCount。
  • poolSize。
  • queueSize。
  • completedTaskCount。
  • rejectCount。
  • taskCost。

没有这些指标,线上只能靠日志猜。

常见根因

队列过大

很多系统把队列设置成几万甚至无界队列。问题是任务堆积时不会立即失败,而是延迟越来越大,最终可能 OOM。

建议:

  • 使用有界队列。
  • 配置拒绝策略。
  • 监控队列长度。
  • 对延迟敏感任务单独线程池。

慢任务占满线程

例如发送通知线程池里混入了文件上传、第三方调用、报表生成。慢任务占住线程后,普通通知也发不出去。

处理方式:

  • 按业务隔离线程池。
  • IO 密集和 CPU 密集分开。
  • 核心链路和非核心链路分开。

拒绝策略不合理

常见拒绝策略:

  • AbortPolicy:直接抛异常。
  • CallerRunsPolicy:调用线程执行。
  • DiscardPolicy:直接丢弃。
  • DiscardOldestPolicy:丢弃最旧任务。

生产中不能随便丢任务。重要任务要落库或发 MQ,拒绝后可补偿。

Future 阻塞

异步任务提交后立刻 get(),本质上还是同步等待。多个任务相互等待时还可能造成死锁。

处理方式:

  • 使用 CompletableFuture 编排。
  • 设置超时时间。
  • 避免线程池内任务等待同一个线程池的其他任务。

参数调优思路

线程池参数要结合任务类型:

任务类型 特征 配置思路
CPU 密集 计算多 线程数接近 CPU 核数
IO 密集 等待多 线程数可适当增大
第三方调用 延迟不可控 独立线程池、短超时
批处理 吞吐优先 控制批量和并发

参数不是一次配置永久不变,要通过压测和线上指标调整。

生产止血

  • 暂停低优先级任务。
  • 增加消费者实例或线程池大小。
  • 对入口限流,避免继续堆积。
  • 临时缩短第三方调用超时。
  • 将任务转 MQ 异步削峰。
  • 对可丢弃任务降级。

长期治理

  • 线程池统一封装和命名。
  • 指标上报到监控系统。
  • 设置队列告警和拒绝告警。
  • 业务线程池隔离。
  • 任务超时控制。
  • 拒绝后补偿机制。

高频面试题

线程池打满怎么排查?

看 activeCount、queueSize、rejectCount 和任务耗时,再结合线程栈确认线程在执行什么任务,是慢任务、阻塞等待、下游超时还是参数配置不合理。

队列越大越好吗?

不是。队列过大会掩盖问题,导致延迟持续增加,严重时 OOM。生产上更推荐有界队列、监控和明确拒绝策略。

为什么要线程池隔离?

不同业务的耗时和优先级不同。共用线程池会导致低优先级慢任务拖垮核心任务,隔离后可以单独限流、降级和扩容。

总结

线程池排查重点是任务是否堆积、线程在执行什么、拒绝后怎么处理。面试回答要能从参数讲到隔离、监控、止血和补偿。