做后端开发这么多年线程池算是最基础、最常用的组件了。不管是业务异步处理、接口并行查询还是定时任务拆解基本都会用到ThreadPoolExecutor。按理说这东西入门简单网上教程一抓一大把参数、原理、源码解析到处都有。但真实线上场景里绝大多数线程池问题都不是不会用而是自以为会用。前段时间项目迭代上线后线上频繁出现接口响应超时、服务CPU飙高、监控面板线程池任务堆积告警。排查了整整两天最后发现全是线程池配置和使用习惯的老问题没有任何技术难点全是经验坑。今天抽空复盘下把这次线上事故、以及过往几年踩过的所有线程池坑一次性整理出来给正在搬砖的小伙伴避个雷。内容都是实战落地总结不扯虚的理论不讲教科书废话。本文基于Java原生线程池SpringThreadPoolTaskExecutor同理适用所有SpringBoot后端项目一、先说本次线上故障的核心问题本次出问题的服务核心业务是批量处理用户数据每小时会触发一次大批量的数据统计、入库、推送逻辑。开发阶段本地测试完全没问题压测也没测出异常结果上线跑了三天凌晨低峰期还好一到白天业务高峰直接频繁爆出任务堆积线程池队列满员新任务直接被拒绝。最初排查以为是代码逻辑卡顿、数据库慢查询导致的抓了日志、看了链路追踪发现单个任务执行耗时并不高平均也就几十毫秒。那问题到底出在哪翻看线程池配置代码瞬间就无语了。先贴一下当时出错的配置线上错误代码// 错误示例线上踩坑代码 ThreadPoolExecutor threadPool new ThreadPoolExecutor( 10, 50, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(100) );很多人看到这配置估计一眼就能看出问题但我当时写代码的时候纯属随手填的参数想当然认为核心线程10最大线程50队列100完全够用了。真实线上打脸来的飞快。我之前一直误解了线程池的执行逻辑以为任务多了就会立刻扩容最大线程数。实际上线程池的扩容机制是先塞满队列再创建非核心线程。也就是说上面这个配置在线程数达到10个核心线程后后续所有新任务都会先丢进容量100的队列里。只有队列彻底塞满之后才会继续创建线程扩容到50个。本次业务高峰期瞬时并发任务量能达到200。10个核心线程快速处理任务但是任务进来的速度远远快于执行结束的速度。队列瞬间打满100这时候才开始扩容线程。但扩容也是需要时间的瞬时并发冲击下扩容速度跟不上任务请求速度直接触发拒绝策略大量任务丢弃、重试最终导致业务逻辑错乱、接口超时。更坑的是我们项目里没有自定义拒绝策略用的是默认的AbortPolicy直接抛异常不做任何兜底。这也是导致故障放大的关键原因。二、复盘那些年所有人都容易踩的线程池低级坑这次故障之后我翻了下团队过往项目代码发现几乎半数新人、甚至部分老开发对线程池的使用都存在误区。很多知识点看似基础实操起来百分百翻车。1、盲目使用Executors快捷创建线程池这是新手最容易犯的错也是阿里开发手册明确禁止的写法。但还是有很多人为了省事直接一行代码创建线程池。比如Executors.newFixedThreadPool(10); Executors.newCachedThreadPool();为什么禁止newFixedThreadPool 底层是无界队列任务量大的时候无限往队列里塞直接堆内存溢出OOM。newCachedThreadPool 最大线程数是Integer.MAX_VALUE瞬时并发上来会创建海量线程直接打满操作系统线程资源导致服务卡死、宕机。很多人觉得自己业务量小不会出问题。但线上业务是动态增长的今天没流量不代表下个月不会突发流量。一旦出问题就是线上重大事故完全没必要省这几行代码。2、核心线程数配置凭感觉不结合业务场景很多人配置核心线程数要么写10、要么写20完全随缘网上抄的配置直接照搬根本不区分IO密集型、CPU密集型业务。这里说下实战里的配置思路不用记复杂公式够用、稳定、好维护就行。CPU密集型任务数据计算、解析、加密核心线程数配置为CPU核心数1即可。配置多了没用CPU上下文切换会严重拖慢执行效率反而降低吞吐量。IO密集型任务数据库查询、接口调用、文件读写这类任务线程大部分时间在等待IO返回不占用CPU资源核心线程数可以适当放大一般配置CPU核心数*2 ~ 200之间根据并发量微调。我之前见过有人把IO密集型业务线程池核心数设为5结果高峰期大量任务阻塞服务吞吐量极低还找不到原因纯纯是参数配置不懂业务导致的。3、队列容量设置不合理要么无界、要么过小队列是线程池最容易出问题的地方没有之一。第一种极端使用无界队列LinkedBlockingQueue。看似稳妥不会触发拒绝策略不会报错。但代价是任务堆积全部积压在队列里内存持续上涨最终OOM崩服务。而且队列堆积太多任务排查问题极其困难。第二种极端队列容量设置极小甚至为0。就像我本次线上故障队列100的容量面对瞬时高峰完全扛不住。队列太小稍微有点并发波动就直接触发拒绝策略业务容错率极低。实战建议线上一律使用有界队列根据业务峰值并发预留20%-30%的冗余容量不要过大、不要过小。4、不自定义拒绝策略线上问题直接爆炸默认的拒绝策略AbortPolicy直接抛出RejectedExecutionException粗暴拒绝任务没有任何重试、兜底、日志记录。线上一旦触发就是业务报错、数据丢失、流程中断。很多开发自测的时候从来不会压测到极限所以永远发现不了这个问题等到线上出事才后悔。实际项目中一定要自定义拒绝策略1、拒绝任务时打印详细日志任务参数、时间、线程池状态方便后续排查2、增加重试机制非核心任务可以丢弃核心任务必须兜底重试3、可以结合告警任务频繁拒绝直接推送钉钉、企业微信告警提前发现风险。5、线程池不允许使用默认线程工厂线程无命名这是一个小细节但排查问题的时候真的救命。默认线程工厂创建的线程名字全是pool-xxx-thread-xxx。线上服务几十个线程池出问题打堆栈日志根本分不清是哪个业务的线程池、哪个线程阻塞。规范写法是自定义线程工厂给不同业务的线程池设置专属线程名比如user-task-thread、order-push-thread。后续排查堆栈、线程阻塞问题一眼就能定位业务模块节省大量时间。6、任务内部异常不捕获导致线程静默死亡这个坑很多老开发都踩过。线程池执行的任务如果内部抛出未捕获的异常会直接导致当前线程终止。线程池会新建线程补足数量但任务本身执行失败且没有任何日志提示。久而久之就会出现线程数正常、服务无报错日志但业务数据一直缺失、部分任务莫名不执行。所以所有扔进线程池的任务必须手动try-catch捕获异常打印错误日志绝对不能偷懒。三、线上稳定可用的线程池最终配置方案结合本次故障复盘和多年线上实战经验贴一套目前我们团队统一使用的、稳定落地的线程池配置适配绝大多数后端业务直接复制改参数就能用。/** * 业务异步线程池 统一配置 * 适配IO密集型业务数据处理、消息推送、接口异步调用 */ Bean(businessThreadPool) public ThreadPoolExecutor businessThreadPool() { int corePoolSize 16; int maxPoolSize 64; int queueCapacity 200; long keepAliveTime 30L; // 自定义线程工厂指定线程名 ThreadFactory threadFactory new ThreadFactoryBuilder() .setNameFormat(business-async-thread-%d) .setDaemon(false) .build(); // 自定义拒绝策略打印日志 主线程重试执行 RejectedExecutionHandler handler (r, executor) - { log.error(业务线程池任务拒绝当前队列容量{}活跃线程数{}, executor.getQueue().size(), executor.getActiveCount()); // 主线程兜底执行避免任务丢失 r.run(); }; return new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, new ArrayBlockingQueue(queueCapacity), threadFactory, handler ); }简单说下这套配置的优势1、线程命名清晰排查问题高效2、有界队列杜绝OOM风险3、自定义拒绝策略日志完整、任务兜底不丢失4、空闲线程30秒回收节省服务资源5、参数适配常规IO密集型后端业务通用性极强。四、线上线程池日常监控建议很多项目的线程池都是黑盒运行平时不管出问题乱查。想要服务稳定必须做监控。实战里建议监控这几个核心指标1、队列堆积数量持续上涨说明线程处理能力跟不上任务速度需要调优参数2、活跃线程数长期打满最大线程数说明参数配置过小3、任务拒绝次数一旦出现拒绝直接告警提前规避故障4、任务平均执行耗时耗时突增大概率是下游接口、数据库卡顿导致。配合PrometheusGrafana或者SpringBoot监控端点就能实现线程池可视化监控不用等故障爆发才发现问题。五、最后碎碎念几句写这篇文章不是为了讲线程池源码也不是为了搬教科书理论。这些基础知识点随便一搜到处都是。但真正线上能稳住服务的从来不是你会不会背原理而是你能不能避开这些不起眼的小坑。很多开发都觉得线程池简单不屑于深究结果线上出的80%的线程池问题全都是基础使用不规范导致的。技术开发从来不是比谁会的多而是比谁踩的坑少谁的代码更稳。希望这篇复盘能让看到的小伙伴以后在线上开发中避开这些低级错误少熬夜排查bug。后续遇到新的线程池线上问题我也会持续更新这篇博客持续复盘沉淀。