线上线程池满了怎么办?

4 阅读7分钟

线上线程池满了,不能只回答“调大一点”,应该按 先止血、再定位、再治理 来处理。


一、先说结论

线程池满了,本质上说明系统处理能力跟不上任务提交速度。
这时候要做的不是盲目扩大线程池,而是先判断到底是:

  1. 任务执行太慢
  2. 流量突增
  3. 队列太小
  4. 线程数配置不合理
  5. 任务里有阻塞、锁竞争、慢 SQL、慢接口
  6. 下游系统变慢,导致线程被大量占住

二、线上第一步怎么处理:先止血

1)先看拒绝策略有没有生效

如果线程池已经满了,会触发拒绝策略。

常见情况:

  • AbortPolicy:大量报错
  • CallerRunsPolicy:主线程被拖慢,请求RT升高
  • DiscardPolicy:任务丢失
  • DiscardOldestPolicy:旧任务被挤掉

所以第一步要先确认:

  • 有没有大量拒绝异常
  • 有没有任务丢失
  • 有没有接口整体变慢

2)快速看监控指标

重点看这几个:

  • 当前线程池活跃线程数
  • 队列积压长度
  • 任务拒绝次数
  • 单个任务平均执行时间
  • CPU 使用率
  • GC 情况
  • 下游接口 / 数据库耗时

如果你发现:

  • 活跃线程数满了,队列也满了
    说明线程池真的打满了
  • 线程数满了,但 CPU 不高
    很可能是线程都阻塞在 IO、锁、慢 SQL、远程调用上
  • CPU 很高
    可能是计算任务过重,或者线程太多引发频繁切换

3)临时止血手段

线上先恢复服务,比“找最优解”更重要。

方案A:限流

减少新任务进入速度,比如:

  • 接口限流
  • 网关限流
  • 降低批量任务并发
  • 暂停非核心异步任务

这是最有效的止血方式之一。

方案B:降级

把非核心逻辑先关掉,比如:

  • 非关键通知先不发
  • 非关键日志异步处理先关闭
  • 推荐、统计、报表类任务先暂停

方案C:快速失败

如果当前业务允许,宁可让部分请求失败,也不要把整个系统拖死。

比如把拒绝策略切成更容易感知的方式,及时返回“系统繁忙”。

方案D:短期扩容

如果确认机器资源还有余量,可以临时:

  • 增加服务实例数
  • 适当增大线程池参数
  • 临时扩大队列

但这只是缓解,不是根治。
因为如果根因是慢 SQL、锁竞争、下游超时,线程池调再大也只是把问题放大。


三、然后定位根因

线程池满,常见不是“线程池自己的问题”,而是任务执行链路有慢点


1)看任务到底卡在哪

要抓这几个方向:

数据库

  • 有没有慢 SQL
  • 是否缺索引
  • 是否出现连接池耗尽
  • 是否有大事务

RPC / HTTP 调用

  • 下游接口是否超时
  • 重试是否过多
  • 是否串行调用过多服务
  • 是否没有超时控制

锁竞争

  • 是否有 synchronized / ReentrantLock 长时间持有
  • 是否多个线程争同一把锁
  • 是否有热点资源竞争

线程阻塞

  • Thread.sleep
  • Future.get() 长时间等待
  • 外部接口卡住
  • MQ 消费速度过慢

2)看线程 dump

这是很关键的排查手段。

通过线程 dump 可以看:

  • 线程都卡在什么方法
  • BLOCKEDWAITING 还是 TIMED_WAITING
  • 是否大量线程卡在数据库连接获取
  • 是否大量线程卡在某个远程调用
  • 是否死锁

如果很多线程都堆在同一个调用点,基本就能找到根因。


3)看任务设计是否合理

很多线程池打满,不是流量大,而是设计有问题,例如:

  • 一个线程池混用所有任务,轻重任务互相影响
  • 耗时任务和短任务放一起
  • 无超时设置,线程长期不释放
  • 无限重试
  • 大量同步等待异步结果
  • 队列过大导致请求堆积越来越久

四、正确治理思路

1)线程池隔离

不同任务不要共用一个线程池。

比如拆开:

  • 接口请求线程池
  • DB异步处理线程池
  • MQ消费线程池
  • 第三方接口调用线程池
  • 定时任务线程池

这样某一类任务打满,不会拖垮全部业务。


2)加超时控制

尤其是 IO 型任务。

例如:

  • 数据库查询超时
  • HTTP/RPC 超时
  • Future 获取结果超时

没有超时,线程就可能长期挂死在线上。


3)优化任务执行时间

这是根治核心。

比如:

  • 优化 SQL
  • 减少不必要的远程调用
  • 批量改异步
  • 去掉无意义重试
  • 减少锁粒度
  • 降低单任务耗时

线程池满,本质往往是任务太慢


4)队列一定要有界

如果队列无界,表面上线程池没满,实际上任务会疯狂堆积,最后变成:

  • 响应越来越慢
  • 内存越来越高
  • 最终 OOM

所以生产环境更推荐:

  • 有界队列
  • 明确拒绝策略
  • 配监控告警

5)配好拒绝策略

不要让线程池满了以后“悄悄出事”。

一般建议:

  • 核心业务:AbortPolicy,快速失败并告警
  • 允许反压:CallerRunsPolicy
  • 可丢任务:明确记录日志再丢弃,不要静默丢

6)做好监控和告警

至少监控这些:

  • corePoolSize
  • maxPoolSize
  • activeCount
  • queueSize
  • completedTaskCount
  • rejectCount
  • task耗时P95/P99

否则线程池打满前你根本不知道。


五、面试推荐回答

如果线上线程池满了,我会先做止血和定位。止血阶段先看拒绝策略影响,确认是否有任务丢失、请求报错或RT升高,同时查看线程池活跃线程数、队列积压、拒绝次数、CPU、GC以及下游接口和数据库耗时。必要时先做限流、降级、快速失败或临时扩容,避免系统继续恶化。

然后我会进一步定位根因,重点排查任务执行时间为什么变长,比如慢 SQL、远程调用超时、锁竞争、连接池不足、没有设置超时、线程阻塞等问题,并结合线程 dump 看线程都卡在哪。

治理上我会从几个方面做优化:第一,按业务类型做线程池隔离,避免互相影响;第二,给数据库和远程调用加超时;第三,优化慢任务,降低单次执行耗时;第四,使用有界队列并配置合适的拒绝策略;第五,建立线程池监控和告警。因为线程池满往往不是单纯参数问题,而是系统吞吐能力和任务提交速度失衡导致的。


六、现场回答版本

“线上线程池满了,我不会第一反应就是调大线程数,因为这通常只是表象。我的处理思路一般是先止血,再定位,再治理。先看线程池活跃线程数、队列积压、拒绝次数、接口 RT、CPU 和下游耗时,必要时先限流、降级或者快速失败,避免系统雪崩。然后通过日志、监控和线程 dump 判断线程到底卡在慢 SQL、远程调用、锁竞争还是连接池获取上。最后再做针对性优化,比如线程池隔离、加超时、优化慢任务、控制队列长度、调整拒绝策略,并补充监控告警。因为线程池满本质上说明消费能力跟不上生产速度,根因往往在任务执行链路,而不只是线程池参数本身。”


七、面试官继续追问“能不能直接调大线程池”时,你可以这样答

可以作为短期缓解手段,但不能作为根本方案。

原因是:

  • 如果是慢 SQL,线程越多,数据库压力越大
  • 如果是下游接口慢,线程越多,堆积越严重
  • 如果是锁竞争,线程越多,争抢越激烈
  • 如果是 CPU 打满,线程越多,上下文切换越严重

所以调大线程池只能在机器资源充足且确认瓶颈不在下游时临时使用。


八、总结

线上线程池满了,我会先通过限流、降级、快速失败等方式止血,再结合监控和线程 dump 排查是慢 SQL、慢接口、锁竞争还是参数配置问题,最后通过线程池隔离、超时控制、任务优化、有界队列和监控告警来治理,而不是简单粗暴地把线程数调大。