Java线程池生产踩坑实录:本地跑毫无问题,上线之后CPU飙高、任务丢失、线程泄露

Java线程池生产踩坑实录:本地跑毫无问题,上线之后CPU飙高、任务丢失、线程泄露
前言
线程池是Java后端开发里再熟悉不过的组件,几乎所有项目都会用到线程池做异步处理、批量任务、消息消费。很多同学写代码的时候直接复制网上的示例,随便设置几个参数就投入业务。
在本地开发、测试环境,任务量不大,并发不高,所有逻辑跑的顺风顺水。一旦部署到生产,流量上来之后,就会遇到各种匪夷所思的现象:CPU瞬间打满、任务莫名消失不执行、线程数量无限膨胀、程序运行一段时间之后直接卡死。

这些问题绝大多数并不是JDK的bug,而是开发者对线程池核心参数、拒绝策略、任务异常处理、线程生命周期理解不到位。测试环境任务少,掩盖了全部隐患,等到线上高并发才集中爆发。下面全部来自线上故障复盘,包含错误代码、故障现象、根因分析、生产可直接落地的修复方案。

QQ20260810-111827

坑1:直接使用Executors快捷工厂创建线程池,线上埋下致命隐患
故障现象
业务使用Executors.newFixedThreadPool()Executors.newCachedThreadPool()快速创建线程池。短时间大量任务涌入,出现OOM内存溢出,或者线程数量疯狂上涨,耗尽服务器资源。

❌错误代码
java
//禁止生产环境直接使用Executors创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);

根因分析
newFixedThreadPool底层队列是LinkedBlockingQueue,无界队列,队列可以无限存放任务。当处理速度跟不上任务提交速度,任务不断堆积,队列对象越来越多,内存持续上涨,最终触发OOM。
newCachedThreadPool线程最大数为Integer.MAX_VALUE,任务处理慢的时候会疯狂新建线程,大量线程抢占CPU,操作系统上下文切换爆炸,服务器直接卡死。
阿里巴巴Java开发手册明确禁止生产使用Executors工具类创建线程池,就是出于这个原因。

✅修复方案
生产环境手动new ThreadPoolExecutor,手动指定核心线程数、最大线程数、队列、拒绝策略,全部参数业务可控。
java
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
4,
8,
30L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new ThreadPoolExecutor.CallerRunsPolicy()
);

实战经验:队列不要设置无界,一定要设置有界队列;拒绝策略不能随便选AbortPolicy,要结合业务场景评估。

坑2:线程池中任务抛出异常,直接静默消失,没有任何日志输出
故障现象
把Runnable任务提交线程池执行,任务内部抛出异常。没有看到任何报错日志,任务悄无声息失败,业务功能失效。本地调试直接run可以看到异常,放到线程池里面异常直接“消失”。

❌错误代码
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,4,10,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
pool.submit(() -> {
int a = 1/0;
});

根因分析
使用submit()提交任务的时候,异常会被封装到Future对象内部,如果不去调用future.get(),异常不会打印到日志。很多开发直接submit,不接收返回值,异常直接被吞掉,线上出问题完全没有排查线索。
如果使用execute方式提交,异常会打印线程池内部线程的堆栈,但是线上很多人习惯用submit,踩中这个坑。

✅修复方案
方案1:优先使用execute提交Runnable任务,异常可以正常输出堆栈;
方案2:submit提交,一定要捕获Future返回对象,调用get捕获异常;
方案3:任务内部手动try‑catch,所有业务逻辑包裹,打印完整异常日志。
java
pool.execute(() -> {
try {
int a = 1/0;
}catch (Exception e){
log.error("线程池任务执行异常",e);
}
});

坑3:拒绝策略选择错误,高并发场景任务直接丢弃,业务无声丢失
故障现象
线程池队列满,达到最大线程数。大量业务任务直接丢掉,业务逻辑没有执行,没有告警,业务数据缺失。默认拒绝策略AbortPolicy抛出RejectedExecutionException,如果上层没有捕获,直接抛出;很多人误以为DiscardPolicy会重试,实际直接丢弃任务,不会任何通知。

❌错误配置
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,4,10,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
new ThreadPoolExecutor.DiscardPolicy() //直接丢弃任务,无日志无告警
);

根因:DiscardPolicy、DiscardOldestPolicy,直接丢弃任务,不会抛出异常。业务层感知不到任务丢失,线上会出现很难排查的隐性业务bug。

✅修复方案

  1. 允许主线程帮忙执行任务,选用CallerRunsPolicy,提交任务的线程自己执行任务,起到限流效果;
  2. 如果业务不允许丢失任务,自定义拒绝策略,拒绝的时候打印error日志、推送告警,把任务存入消息队列或者本地磁盘,后续补偿重试;
  3. 核心业务禁止直接使用DiscardPolicy。

自定义拒绝策略示例:
java
class AlertRejectPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.error("线程池任务被拒绝,队列已满,请检查业务处理速度");
//此处可以接入告警,把任务存入MQ做补偿
throw new RejectedExecutionException("任务被拒绝");
}
}

坑4:线程池没有优雅关闭,服务停机任务直接中断,业务数据损坏
故障现象
项目重启、服务发布,正在运行的线程池任务被粗暴终止,正在处理的批量任务、数据同步任务执行一半中断,产生残缺数据。很多项目只创建线程池,应用关闭的时候没有做shutdown处理。

❌错误场景:Spring容器销毁,线程池没有执行关闭,正在执行的任务直接被杀死。

✅修复方案
Spring项目把线程池交给Spring管理,使用@Bean方式创建,Spring销毁时自动执行shutdown;手动创建线程池,应用关闭钩子执行shutdown+awaitTermination。
java
threadPool.shutdown();
try {
if (!threadPool.awaitTermination(30, TimeUnit.SECONDS)) {
threadPool.shutdownNow();
}
} catch (InterruptedException e) {
threadPool.shutdownNow();
}

注意:shutdownNow会尝试中断正在执行的任务,业务代码需要处理InterruptedException,做好业务回滚。

坑5:核心线程数设置不合理,参数拍脑袋写死,性能上不去
故障现象
IO密集型业务,核心线程设置为CPU核心数,线程数量太少,大量IO等待,服务器CPU利用率很低,接口吞吐量上不去。很多人照搬网上公式,不分业务场景直接套用。

原理区分
CPU密集任务:大量计算,少IO,线程数接近CPU核心数;
IO密集任务:数据库查询、RPC、http调用,大量时间在等待,线程数需要设置更大。

很多业务大部分都是IO密集,直接设置核心线程等于CPU核心数,大量请求排队等待,资源完全没有利用起来。

✅修复方案
区分业务场景,不要网上复制参数直接用。线上做好监控,监控队列长度、活跃线程数、任务平均耗时,根据监控指标动态调参。

坑6:ThreadLocal在线程池复用线程场景发生数据错乱
故障现象
线程池线程复用,ThreadLocal存储用户id、上下文信息,任务执行拿到别的请求的上下文数据,出现用户身份错乱,偶现bug,很难复现。

❌错误逻辑:线程池线程反复复用,ThreadLocal变量不会自动清空,上一个任务存入的数据,会被下一个任务读到。

✅修复方案
每个任务执行完毕,必须手动remove ThreadLocal。
java
pool.execute(()->{
try{
UserContextHolder.setUserId(1001L);
//执行业务
}finally {
UserContextHolder.remove();
}
});

坑7:线程池没有监控告警,线上出问题后知后觉
很多项目线程池上线之后,完全没有监控。队列堆积、线程耗尽、大量任务拒绝,运维开发完全不知道,等到业务报错才发现问题。

✅生产实践
监控指标:活跃线程数、队列任务数量、完成任务总数、拒绝任务计数。拒绝任务计数一旦大于0就要触发告警。

总结
线程池看着简单,参数背后藏着非常多细节。很多开发只学会怎么提交任务,没有理解队列、拒绝策略、线程复用、异常处理。本地测试任务量小,很多问题不会暴露。上线前一定要思考:任务满了怎么办?任务抛异常怎么办?服务停机任务如何处理?
遇到线程池相关故障,优先看拒绝计数、队列长度、线程活跃数,不要上来就改参数。

友情链接:凡尘博客凡尘影院凡尘乡音|凡尘街坊凡尘博客|雨落凡尘博客|羽落凡尘博客凡尘博客|雨落凡尘博客|羽落凡尘博客

凡尘版权

本文原创,转载请完整保留版权与友链。

posted @ 2026-08-10 11:20  凡尘——雨落凡尘  阅读(3)  评论(0)    收藏  举报