Java并发之ThreadPoolExecutor参数到底怎么配详解

 更新时间:2026年07月27日 09:34:43   作者:codedevin  
在java并发编程中,threadpoolexecutor是核心线程池实现类,下面这篇文章主要介绍了Java并发之ThreadPoolExecutor参数到底怎么配的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

很多人用线程池图省事,直接 Executors.newFixedThreadPool(10)newCachedThreadPool(),上线后要么内存溢出,要么请求被莫名其妙丢弃,排查半天才发现是线程池参数没配对。阿里的《Java 开发手册》甚至明确禁止用 Executors 快捷方法创建线程池。这篇讲清楚 ThreadPoolExecutor 七个参数各自的作用、任务进来后的执行流程,以及生产环境到底该怎么配。

一、为什么不用 Executors 快捷方法

先看 Executors.newFixedThreadPool 的实现:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>()); // 无界队列!
}

问题出在 LinkedBlockingQueue 默认容量是 Integer.MAX_VALUE,等于无界。当任务生产速度超过消费速度,队列会无限堆积,最终 OOM。newCachedThreadPool 更极端,最大线程数是 Integer.MAX_VALUE,突发流量下会创建海量线程直接压垮机器。所以要手动 new,把每个参数都攥在手里。

二、七个参数逐个拆解

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                              // corePoolSize 核心线程数
    8,                              // maximumPoolSize 最大线程数
    60L, TimeUnit.SECONDS,          // keepAliveTime 空闲线程存活时间
    new ArrayBlockingQueue<>(200),  // workQueue 有界任务队列
    new ThreadFactoryBuilder()      // threadFactory 线程工厂,给线程起名
        .setNameFormat("order-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy() // handler 拒绝策略
);
  • corePoolSize:常驻线程数,即使空闲也不回收(除非开了 allowCoreThreadTimeOut)。
  • maximumPoolSize:线程数上限,只有队列满了才会创建到这个数。
  • keepAliveTime:超过核心数的那些线程,空闲多久后被回收。
  • workQueue:核心线程都忙时,新任务先进这个队列排队,必须用有界队列
  • threadFactory:给线程起有意义的名字,出问题看线程栈时能一眼定位是哪个池。
  • handler:队列满且线程数到上限后,新任务怎么处理。

三、任务进来后的执行流程(最关键)

这是最反直觉的地方。一个任务提交后,线程池的判断顺序是:

  1. 当前线程数 < corePoolSize → 直接创建新线程执行,哪怕有空闲核心线程也照样创建,直到填满核心数。
  2. 线程数已达 corePoolSize → 任务进队列排队。
  3. 队列满了 → 才创建新线程,直到达到 maximumPoolSize。
  4. 队列满且线程数已达 maximum → 触发拒绝策略

划重点:是"先塞队列,再扩线程"。所以如果你用了无界队列,第 3 步永远到不了,maximumPoolSize 形同虚设。这就是为什么 newFixedThreadPool 的 max 参数没有任何意义。

四、四种拒绝策略怎么选

队列满、线程满之后,JDK 内置四种处理方式:

// 1. AbortPolicy(默认):直接抛 RejectedExecutionException
new ThreadPoolExecutor.AbortPolicy();
// 2. CallerRunsPolicy:让提交任务的线程自己执行,相当于天然限流
new ThreadPoolExecutor.CallerRunsPolicy();
// 3. DiscardPolicy:默默丢弃新任务,不抛异常(危险,任务无声消失)
new ThreadPoolExecutor.DiscardPolicy();
// 4. DiscardOldestPolicy:丢掉队列里最老的任务,再尝试提交
new ThreadPoolExecutor.DiscardOldestPolicy();

生产上最实用的是 CallerRunsPolicy:任务多到处理不过来时,让调用方线程(比如 Tomcat 的请求线程)亲自去跑任务,调用方被拖慢就自然降低了提交速度,形成背压反馈,既不丢任务也不会雪崩。如果任务不能丢,也可以自定义 handler 把任务落库或写入 MQ 后续重试。

五、参数到底配多大

没有万能公式,取决于任务是 CPU 密集还是 IO 密集:

  • CPU 密集型(加密、压缩、计算):线程数 ≈ CPU 核数 + 1。线程再多只会增加上下文切换开销。
  • IO 密集型(调接口、查库、读文件):线程大部分时间在等 IO,可以配更多,经验公式 核数 * (1 + 平均等待时间/平均计算时间)。比如 8 核、任务 90% 时间在等 IO,可以配到几十。

队列大小则要结合业务能接受的排队延迟和内存来定。一个务实做法:先按经验配,再上线用监控盯着调。

// 暴露线程池运行指标,接入 Prometheus / 日志定期打印
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
    System.out.printf("活跃=%d 池大小=%d 队列积压=%d 已完成=%d%n",
        executor.getActiveCount(),
        executor.getPoolSize(),
        executor.getQueue().size(),
        executor.getCompletedTaskCount());
}, 0, 5, TimeUnit.SECONDS);

队列长期积压说明消费能力不足,要加线程或优化任务耗时;活跃数长期打满 max 说明池子偏小。用数据说话,别拍脑袋。

六、别忘了优雅关闭

线程池用完要关,否则非守护线程会阻止 JVM 退出:

executor.shutdown(); // 不再接收新任务,等已提交任务跑完
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
    executor.shutdownNow(); // 超时后强制中断
}

shutdown() 是温和的,shutdownNow() 会给正在跑的线程发中断信号并返回未执行的任务列表。

七、小结

  • 别用 Executors 快捷方法,手动 new ThreadPoolExecutor 并用有界队列,防 OOM。
  • 记住执行顺序:核心线程 → 进队列 → 扩到最大线程 → 拒绝策略。是"先排队再扩容"。
  • 无界队列会让 maximumPoolSize 失效,这是最常见的误配。
  • 拒绝策略生产首选 CallerRunsPolicy,靠背压限流;不能丢的任务自定义 handler 落库重试。
  • CPU 密集配核数+1,IO 密集可放大;配完一定要上监控持续调。

一句话记忆:线程池的坑几乎都出在"队列无界"和"不懂先排队再扩容"这两点上,把这两条想明白,参数就配对了一大半。

到此这篇关于Java并发之ThreadPoolExecutor参数到底怎么配的文章就介绍到这了,更多相关Java并发ThreadPoolExecutor参数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • IDEA创建SpringBoot的maven项目的方法步骤

    IDEA创建SpringBoot的maven项目的方法步骤

    这篇文章主要介绍了IDEA创建SpringBoot的maven项目的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • mybatis 返回Map类型key默认为大写问题

    mybatis 返回Map类型key默认为大写问题

    这篇文章主要介绍了mybatis 返回Map类型key默认为大写问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Java中空指针异常的几种解决方案

    Java中空指针异常的几种解决方案

    这篇文章主要介绍了Java中空指针异常的几种解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-01-01
  • Java接口自动化测试框架设计之Get请求方法和测试详解

    Java接口自动化测试框架设计之Get请求方法和测试详解

    这篇文章主要介绍了Java接口自动化测试框架设计 Get请求方法和测试,框架设计我们只是介绍基本的组件,而且框架设计没有想象那么难,一步一步跟着做就会了。这篇我们来演示,如果通过Java代码来实现一个用纯代码实现Http中的Get请求过程,需要的朋友可以参考下
    2019-07-07
  • SpringBoot日期格式转换之配置全局日期格式转换器的实例详解

    SpringBoot日期格式转换之配置全局日期格式转换器的实例详解

    这篇文章主要介绍了SpringBoot日期格式转换之配置全局日期格式转换器的实例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • Java线程安全的常用类_动力节点Java学院整理

    Java线程安全的常用类_动力节点Java学院整理

    在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的。在jdk1.2之后,就出现许许多多非线程安全的类。 下面是这些线程安全的同步的类
    2017-06-06
  • Java多线程中Future.get () 的潜在陷阱

    Java多线程中Future.get () 的潜在陷阱

    本文介绍了在Java多线程应用程序中使用Future进行异步任务时可能出现的阻塞问题,并提供了解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2026-01-01
  • Java对象和JSON字符串之间的转换方法(全网最清晰)

    Java对象和JSON字符串之间的转换方法(全网最清晰)

    这篇文章主要介绍了如何在Java中使用Jackson库将对象转换为JSON字符串,并提供了一个简单的工具类示例,该工具类支持基本的转换功能,文中给出了详细的代码示例,需要的朋友可以参考下
    2025-02-02
  • 详解java内部类的访问格式和规则

    详解java内部类的访问格式和规则

    在本文里我们给大家详细分享了关于java内部类的访问格式和规则知识点内容,有兴趣的朋友们学习下。
    2018-10-10
  • Java自定义数组列表的实现操作

    Java自定义数组列表的实现操作

    这篇文章主要介绍了Java自定义数组列表的实现操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09

最新评论