Skip to content
Java 线程池与异步编排 · 第 1 篇 / 共 4 篇
领域编程与框架
专题Java 专题
当前序列Java 线程池与异步编排
阅读位置第 1 篇 / 共 4 篇当前专题第 4 个序列 / 共 10 个序列

线程池参数怎么配:corePoolSize、maximumPoolSize、queue 和拒绝策略该怎么理解

并发问题里,线程池几乎绕不过去。真正麻烦的地方不是会不会用,而是参数一旦没配对,系统行为很容易和预期完全不一样。

先说结论

如果你只记三点,先记这三点:

  • 不要默认无脑使用 Executors 提供的现成线程池
  • 线程池效果不是只看线程数,而是“核心线程数 + 队列 + 最大线程数 + 拒绝策略”一起决定
  • 线程池参数必须结合任务类型来看,CPU 密集和 IO 密集的配置思路不一样

一、线程池的任务提交流程

理解参数之前,先把执行顺序想清楚。对于 ThreadPoolExecutor,提交一个任务后,大致是这样处理的:

  1. 如果当前运行线程数小于 corePoolSize,优先创建核心线程
  2. 如果核心线程够了,任务先尝试进入阻塞队列
  3. 如果队列满了,再看能不能继续创建线程,直到 maximumPoolSize
  4. 如果线程数已经到上限且队列也满了,就执行拒绝策略

这意味着一个非常关键的事实:

队列类型不同,maximumPoolSize 的意义会很不一样。

二、四个最核心参数分别控制什么

1. corePoolSize

核心线程数,表示线程池愿意长期保留多少个线程。

它更像“基础产能”。

2. maximumPoolSize

线程池允许扩张到的最大线程数。

它更像“高峰兜底上限”。

3. workQueue

任务排队的地方。

常见选择有:

  • LinkedBlockingQueue
  • ArrayBlockingQueue
  • SynchronousQueue

4. RejectedExecutionHandler

当线程池真的处理不过来时,最后怎么兜底。

常见策略:

  • AbortPolicy:直接抛异常
  • CallerRunsPolicy:让提交任务的线程自己执行
  • DiscardPolicy:直接丢弃
  • DiscardOldestPolicy:丢掉最旧的任务

三、为什么不推荐直接用 Executors

很多人刚学线程池时会先接触:

  • Executors.newFixedThreadPool()
  • Executors.newCachedThreadPool()
  • Executors.newSingleThreadExecutor()

它们用起来很方便,但工程里更稳妥的做法通常是直接显式创建 ThreadPoolExecutor

原因主要有两个:

  • 某些默认队列可能过大,容易把压力堆到内存上
  • 某些默认线程扩张策略太激进,容易在高峰期把机器拖垮

特别是:

  • 无界队列容易导致任务大量堆积
  • 线程数无限扩张容易导致上下文切换和资源竞争恶化

四、队列选型比很多人想象中更重要

LinkedBlockingQueue

特点:

  • 容量可以很大,甚至默认接近无界
  • 更适合任务堆积可接受、但不希望线程数快速膨胀的场景

风险:

  • 流量顶上来时,问题可能不是立刻报错,而是请求越积越多
  • 最终表现成延迟上升、堆内存上涨、系统整体变慢

ArrayBlockingQueue

特点:

  • 有明确容量
  • 更适合希望把压力边界控制清楚的场景

优点:

  • 更容易做容量治理
  • 系统满载时行为更可预期

SynchronousQueue

特点:

  • 不真正存储任务
  • 提交一个任务,必须马上交给线程处理

这意味着:

  • 如果没有空闲线程,就会继续创建线程,直到 maximumPoolSize
  • 它适合吞吐优先、短任务较多的场景,但对参数控制要求更高

五、CPU 密集和 IO 密集,配置思路为什么不同

CPU 密集型任务

例如:

  • 复杂计算
  • 加解密
  • 本地批量转换

这类任务主要耗 CPU,线程太多反而会增加切换成本。

常见思路:

  • 核心线程数接近 CPU 核数
  • 队列不要太大
  • 拒绝策略要明确

IO 密集型任务

例如:

  • 调远程接口
  • 查数据库
  • 读写文件

这类任务大量时间在等待外部资源,所以线程数通常可以比 CPU 核数更高一些。

但也不能无脑加大,因为最终仍会受数据库连接数、下游服务容量和机器内存限制。

六、一个更稳妥的配置模板

java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        8,
        16,
        60L,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(200),
        new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

这段配置的思路是:

  • 有一个稳定的基础并发能力
  • 高峰期允许适度扩容
  • 队列容量可控
  • 压力过大时通过 CallerRunsPolicy 反向限制调用方速度

实际数值不一定照抄,但这种“可控扩容 + 有界队列 + 明确兜底”的思路通常比默认配置更稳。

七、线程池最常见的几个误区

1. 只看线程数,不看队列

线程池处理能力不是由 maximumPoolSize 单独决定的。

如果队列是无界的,很多时候任务根本不会触发扩容。

2. 觉得队列越大越安全

队列大只是把问题延后,不一定是真正解决。

如果下游持续处理不过来,大队列只会把延迟和内存风险放大。

3. 拒绝策略随便选

拒绝策略其实是系统过载时的最后一道行为定义。

它直接决定:

  • 是快速失败
  • 是降速自保
  • 还是悄悄丢任务

4. 一个线程池跑所有任务

不同任务的执行时间、阻塞特性和优先级差异很大,混在一起容易相互拖垮。

更稳妥的做法通常是按业务类型拆分线程池。

一句话总结

线程池参数不是孤立配置项,而是一组容量治理规则。

真正要配的不是“多少线程”,而是系统在高峰期准备怎样排队、怎样扩容、怎样拒绝,以及出问题时怎样可控地退化。

延伸阅读相关文章优先当前专题,再补跨专题关联。
同一序列 · 顺着当前主线继续读线程池拒绝策略怎么选适合把线程池参数、拒绝策略、CompletableFuture 和 ForkJoin 放在一起连续看。Java 专题 · Java 线程池与异步编排同一序列 · 顺着当前主线继续读CompletableFuture 实战适合把线程池参数、拒绝策略、CompletableFuture 和 ForkJoin 放在一起连续看。Java 专题 · Java 线程池与异步编排同专题其他序列 · 共享标签:并发CAS 的 ABA 问题怎么理解适合把 synchronized、CAS、LongAdder 和并发容器的适用边界放到一起看。Java 专题 · Java 锁实现与并发细节同专题其他序列 · 共享标签:并发false sharing 为什么会拖慢并发程序适合把 happens-before、volatile、原子类和阶段式同步器放在一条并发语义主线上理解。Java 专题 · Java 并发语义与同步器细节跨专题关联 · 同场景:基础学习autocommit 为什么也会影响锁行为适合把 next-key lock、一致性读、复制延迟和分库分表边界放回同一条一致性主线上判断。MySQL 专题 · MySQL 事务、锁与高可用边界跨专题关联 · 同场景:基础学习@Conditional 系列注解怎么配合使用适合把自动装配、条件装配、Profile 和循环依赖放回 Spring Boot 启动过程里理解。Spring 专题 · Spring Boot 启动、装配与配置
继续阅读Java 线程池与异步编排当前序列第 1 篇 / 共 4 篇当前专题第 4 个序列 / 共 10 个序列
往前看
上一序列Java 并发与锁机制从第 1 篇开始:JUC 总览
往后看
下一篇线程池拒绝策略怎么选继续当前序列下一章下一序列JVM 与进程排障从第 1 篇开始:JVM 内存问题怎么分析

把零散经验整理成可查、可复用、可持续更新的企业级知识门户