很多线上问题看起来是接口慢、CPU 高、任务堆积,最后排查一圈才发现根因都绕不开线程池。线程池本身并不复杂,真正容易出问题的是:参数拍脑袋、队列无限长、拒绝策略没兜底、监控缺失,以及业务方误以为“线程越多越快”。
线程池治理的核心不是背几个参数,而是理解任务从提交到执行完成的整条链路。
一个任务进入线程池后,大致会经历这几个阶段:先判断当前工作线程数是否小于 corePoolSize;如果小于,就创建核心线程执行任务;如果核心线程已满,任务进入阻塞队列;如果队列也满了,才继续创建非核心线程,直到 maximumPoolSize;如果线程数也达到上限,就触发拒绝策略。
所以线程池调优不能只看 corePoolSize 和 maximumPoolSize,还要把队列、任务耗时、提交速度、拒绝策略放在一起看。
第一,线程池参数要结合任务类型。
如果任务主要是 CPU 密集型,比如大量计算、加解密、压缩、规则匹配,那么线程数不应该远大于 CPU 核数。线程太多只会增加上下文切换,让 CPU 更忙,但吞吐不一定提升。
如果任务主要是 IO 密集型,比如 RPC 调用、数据库访问、文件读写、网络请求,那么线程数可以适当放大。因为线程大部分时间在等待外部资源,增加线程数可以提高并发处理能力。
但这里有一个常见误区:IO 密集型也不是线程越多越好。线程数放大后,下游服务、数据库连接池、Redis 连接池也要能承受,否则只是把压力从应用内部转移到了下游。
第二,不要轻易使用无界队列。
很多人创建线程池时喜欢用 LinkedBlockingQueue,而且不设置容量。这样看起来“不会丢任务”,但风险很大。提交速度一旦超过消费速度,任务会持续堆积,内存逐渐上涨,最终可能导致 Full GC 频繁甚至 OOM。
生产环境更推荐使用有界队列。队列容量本质上是系统能承受的缓冲区大小,它应该由业务延迟要求、单任务耗时、峰值流量共同决定。
比如一个任务平均耗时 100ms,线程池有 20 个工作线程,理论上每秒可以处理约 200 个任务。如果峰值提交量达到每秒 1000 个,而队列容量设置成 10000,看似能扛一会儿,但后面的任务可能已经排队几十秒。这对用户请求类任务来说没有意义,因为任务还没执行就已经超时了。
第三,拒绝策略必须有业务兜底。
线程池触发拒绝,不一定是坏事。它至少说明系统已经识别到自己扛不住了。真正危险的是拒绝后没有日志、没有指标、没有降级,导致业务悄悄丢任务。
常见拒绝策略有几种:
AbortPolicy 会直接抛异常,适合必须感知失败的场景。
CallerRunsPolicy 会让提交任务的线程自己执行任务,可以形成一定反压,但要小心把主流程拖慢。
DiscardPolicy 会静默丢弃任务,生产环境一般不建议直接使用。
DiscardOldestPolicy 会丢弃队列中最旧的任务,再尝试提交新任务,适合少数允许覆盖旧任务的场景。
实际项目中,更推荐自定义拒绝策略:记录关键日志,打点监控指标,必要时写入补偿队列,或者返回明确的降级结果。
第四,线程池一定要可观测。
没有监控的线程池,线上排查基本靠猜。至少要关注这些指标:
当前线程数、活跃线程数、核心线程数、最大线程数、队列长度、队列剩余容量、任务完成总数、拒绝次数、任务执行耗时、任务排队耗时。
其中“排队耗时”很容易被忽略,但它非常关键。很多接口慢,并不是业务执行慢,而是任务在线程池里等了太久。只看任务执行耗时,会误判问题。
可以在任务提交时记录时间,在真正执行前计算等待时间。这样就能区分“队列堆积导致慢”和“业务逻辑本身慢”。
第五,不同业务要隔离线程池。
一个常见线上事故是:多个业务共用同一个线程池。某个低优先级任务突然堆积,把线程池占满,结果核心接口也被拖垮。
线程池隔离是非常实用的治理手段。比如用户请求、异步通知、报表任务、文件处理、消息消费,最好根据重要程度和资源消耗拆分不同线程池。
这样即使某一类任务异常,也不会把整个应用的异步能力全部打满。
第六,线程池问题的排查思路。
如果接口变慢,先看线程池队列长度和活跃线程数。如果活跃线程数接近最大线程数,队列持续增长,说明消费能力不足或下游变慢。
如果 CPU 很高,要看线程数是否过大,是否有大量线程争抢锁,是否存在频繁上下文切换。
如果任务大量失败,要看拒绝次数、异常日志、下游超时和连接池耗尽情况。
如果内存上涨,要重点检查无界队列、任务对象大小,以及任务是否携带大量上下文数据。
线程池不是一个单独的性能开关,而是业务流量、任务耗时、下游能力和系统资源之间的平衡器。真正稳的线程池配置,不是某个固定公式算出来的,而是在明确业务特征后,通过压测、监控和线上反馈逐步校准出来的。
总结一下:线程池治理要看五件事。参数是否匹配任务类型,队列是否有界,拒绝策略是否可感知,监控指标是否完整,业务之间是否做好隔离。做到这些,线程池才不只是“能跑”,而是能在高峰和异常场景下稳住系统。