Java 线程池线上高频踩坑记录

线程池看着简单,日常开发人人都在用,但真正出问题的场景基本都是参数乱配、使用姿势不对导致的。

很多人习惯直接用工具类默认线程池、或者随便写个核心参数,本地测试毫无问题,压测、高并发场景直接雪崩。

记录几个最近线上真实遇到、重复性极高的线程池坑,附带根因和修复代码,可直接对照整改。


  1. 直接使用 Executors 静态工厂创建线程池
    这是阿里开发手册明令禁止的写法,但项目里依旧随处可见。
    // 错误写法
    ExecutorService executor = Executors.newFixedThreadPool(10);
    ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
    线上问题根因:
    newFixedThreadPool 和 newSingleThreadExecutor 的任务队列是 Integer.MAX_VALUE 无界队列。
    高并发、任务堆积时,队列无限扩容,直接导致堆内存飙升、GC 频繁、最终 OOM。
    而 newCachedThreadPool 是线程数无上限,突发流量会疯狂创建线程,直接打满 CPU、导致服务卡死。
    规范写法:手动定义 ThreadPoolExecutor,指定有界队列和拒绝策略
    // 业务自定义线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    5,
    10,
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new ThreadPoolExecutor.CallerRunsPolicy()
    );

  1. 核心线程数设置不合理导致的性能瓶颈
    很多人有一个误区:CPU 密集型就设核心数为 CPU 核心数,IO 密集型随便开大。
    实际业务中大部分接口都是IO 密集型(数据库、Redis、HTTP 调用),线程数过小会导致任务排队、接口 RT 飙升。
    但线程数也不能无脑开大:线程过多会带来大量上下文切换,反而拖垮整体性能。
    实战配置经验:
  • CPU 密集型(计算、解析、排序):核心线程数 = CPU核心数 + 1
  • IO 密集型(DB、缓存、网络请求):核心线程数 = CPU核心数 * 2 ~ 4
    另外核心线程不要设置过大,非核心线程空闲超时时间务必配置,避免闲置线程长期占用资源。

  1. 忽略拒绝策略导致任务静默丢失
    很多自定义线程池不指定拒绝策略,默认使用 AbortPolicy。
    队列满、线程池满载时,直接抛出 RejectedExecutionException,不捕获异常直接任务丢失、接口报错。
    还有一部分项目为了不报错,无脑使用 DiscardPolicy,直接丢弃超额任务,无任何日志、无告警,线上问题极难排查。
    业务最优方案:
    核心业务不允许丢任务,优先使用 CallerRunsPolicy,让主线程自己执行兜底,限流不伤业务;
    非核心任务可以自定义拒绝策略,打印日志+埋点告警,方便监控堆积情况。

  1. 线程池线程未自定义名称,线上排查完全失明
    默认线程池线程名称都是 pool-xxx-thread-xxx。
    线上日志、线程堆栈、GC 日志中,根本无法区分是哪个业务的线程池在阻塞、在堆积,出问题只能盲猜。
    这是非常低成本、高收益的优化点,绝大多数项目都没做好。
    正确写法:自定义线程工厂,绑定业务名称
    // 自定义线程工厂
    ThreadFactory factory = new ThreadFactoryBuilder()
    .setNameFormat("order-pool-%d")
    .setDaemon(false)
    .build();

ThreadPoolExecutor orderThreadPool = new ThreadPoolExecutor(

5,10,60L,TimeUnit.SECONDS,

new ArrayBlockingQueue<>(100),

factory,

new ThreadPoolExecutor.CallerRunsPolicy()

);

出问题直接通过线程名定位到订单业务线程池,排查效率提升数倍。


  1. 任务内部异常未捕获,线程静默退出
    线程池任务抛出未捕获异常时,当前线程会直接终止,线程池会新建一个线程补上。
    看似线程池没崩,但实际问题很大:频繁创建销毁线程、任务异常无日志,业务数据错乱完全无感知。
    错误示范:
    threadPool.execute(() -> {
    // 任意空指针、数组越界、数据库异常
    int a = 1 / 0;
    });
    强制规范:所有线程池任务内部必须 try-catch 全部异常,打印详细日志
    threadPool.execute(() -> {
    try {
    int a = 1 / 0;
    } catch (Exception e) {
    log.error("异步任务执行异常", e);
    }
    });

  1. 服务关闭不优雅,异步任务直接中断
    Spring 项目停机、发布重启时,线程池未主动关闭,正在执行的异步任务直接被中断,导致数据半成品、业务脏数据。
    解决方案:交给 Spring 管理线程池,利用 Bean 销毁钩子优雅关闭
    @Bean
    public ThreadPoolExecutor businessThreadPool() {
    return new ThreadPoolExecutor(5,10,60L,TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100));
    }

// 项目销毁时关闭线程池

@PreDestroy

public void shutdownPool() {

if (!businessThreadPool.isShutdown()) {

businessThreadPool.shutdown();

try {

if (!businessThreadPool.awaitTermination(3, TimeUnit.SECONDS)) {

businessThreadPool.shutdownNow();

}

} catch (InterruptedException e) {

businessThreadPool.shutdownNow();

}

}

}


小结

线程池的坑基本都不是不会用,而是懒得深究底层规则、习惯套用默认写法。

看似简单的参数、拒绝策略、异常捕获,全部是线上高并发场景的致命关键点。日常开发统一规范线程池创建方式,能规避大部分异步线上故障。

相关推荐
苏渡苇3 小时前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring·spring cloud·系统监控·apm
小尹哥-程序员6 小时前
第4集:让AI学会看文档:Spring AI RAG入门实战
java·人工智能·spring
叶总没有会7 小时前
微服务--分布式基础
java·spring·spring cloud·微服务
考虑考虑15 小时前
Springboot环境变量占位符语法
spring boot·后端·spring
步行cgn17 小时前
Spring 基于 XML 的自动装配:byName 详解
java·后端·spring
不会c+17 小时前
Day02 - 需求分析与用例建模
spring
LuTshoes18 小时前
spring ai 实战 手搓 PlaneExecuteAgent
java·人工智能·spring·ai
边境悍匪19 小时前
蜗牛学苑 Java 智能体学习 Day42|项目周开发技术汇总 1 思维导图复盘
java·开发语言·spring boot·学习·spring
paopaokaka_luck20 小时前
小学非遗科普平台(AI 非遗科普问答,ECharts学情数据、非遗资源分类与审核,资源收藏下载与学习记录,学习任务发布和作品评分,活动报名签到与统计)
java·人工智能·spring·信息可视化·数据分析·echarts