maximumPoolSize 参数详解与最佳实践

先问个问题:你知道线程池到底能创建多少个线程吗?很多人脱口而出「corePoolSize」,其实真正决定上限的是 maximumPoolSize。我见过不少线上事故,就是因为对这个参数理解不透,导致了线程暴增甚至 OOM。

说白了,corePoolSize 是「常驻编制」,maximumPoolSize 是「临时工上限」。只有编制满了、队列也装不下了,才会拉临时工来干活。嗯,这个临时工是有数量限制的,maximumPoolSize 就是这个硬上限。

maximumPoolSize 的真实含义

官方定义:线程池允许的最大线程数量。一旦线程数达到这个值,就不会再创建新线程了。这时候再有任务来,只能走拒绝策略。

那它跟 corePoolSize 到底什么关系?我习惯用一个比喻:

  • corePoolSize:正式员工编制,平时就这么多人在岗
  • maximumPoolSize:公司总工位上限,包括正式工和临时工
  • 临时工只在业务高峰期被拉来干活,高峰期一过就会被回收(前提是 allowCoreThreadTimeOut 或者线程空闲时间超过 keepAliveTime)

你看,这两个参数并不是「越大越好」,而是要根据业务负载和资源来平衡。我曾经在一个项目里把 maximumPoolSize 设得跟 corePoolSize 一样大,结果压测时任务全部排队,延迟飙升——那就是把最大线程数设死了,白白浪费了扩容能力。

核心结论:maximumPoolSize 决定了线程池能「撑到多大」——它不是常态,而是应急上限。

扩容时机:从核心到最大

很多新手以为「任务来了就先开线程,直到最大线程数,然后再排队」。错!流程其实是:

  1. 任务提交,先看当前线程数是否小于 corePoolSize?是则创建核心线程执行
  2. 如果核心线程已满,任务尝试放入阻塞队列
  3. 队列满了,才尝试创建新线程——这时的线程数不能超过 maximumPoolSize
  4. 如果队列满了且线程数已达 maximumPoolSize,执行拒绝策略

我刚开始学的时候也觉得这个顺序很奇怪。为什么要先填队列再扩容?你想想看,CPU 密集型的任务,线程多了反而上下文切换更慢,所以先让任务在队列里排队才是合理的。只有队列真的塞不下了,才说明负载已经超出预期,这时候再扩线程来分担。

条件线程数变化任务去向
< corePoolSize创建核心线程立刻执行
≥ corePoolSize不创建入队等待
队列已满 & 线程数 < maximumPoolSize创建非核心线程立刻执行
队列已满 & 线程数 ≥ maximumPoolSize拒绝走拒绝策略
经验之谈:我调优时通常会监控「队列积压量」和「活跃线程数」这两个指标。如果队列持续增长但线程数没到 maximumPoolSize,就说明扩容太慢,可以适当调小队列容量或增大临时线程数。

队列配合机制:不同队列导致扩容差异

上文提到,队列满了才触发扩容。那「队列容量」就直接决定了扩容的难易程度。我个人习惯把队列分成三类来讨论:

  • 无界队列(LinkedBlockingQueue 不带容量):队列永远不会满,所以 maximumPoolSize 形同虚设。线程数永远固定在 corePoolSize,不会创建非核心线程。我之前有个同事把队列设成无界,结果任务积压到几百万,内存直接打满。嗯,无界队列要慎用。
  • 有界队列(ArrayBlockingQueue、LinkedBlockingQueue 带容量):这才是正常的使用方式。队列容量越小,越容易触发扩容。比如容量为 10,核心线程数 5,maximumPoolSize 20。那只要核心线程忙不过来,队列很快满,就会创建最多 15 个临时线程。
  • SynchronousQueue:这个队列不存储任务,直接就相当于「队列容量为 0」。任务提交时队列就「满」了,所以会立即尝试创建非核心线程,直到 maximumPoolSize。适合任务执行时间短且不需要排队等待的场景,比如 RPC 请求处理。
// 伪代码示例:创建线程池,观察扩容行为
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                // corePoolSize
    10,               // maximumPoolSize
    60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10),
    Executors.defaultThreadFactory(),
    new AbortPolicy()
);
// 提交 15 个任务:前 5 个由核心线程执行,中间 10 个入队,最后队列满,触发创建 5 个非核心线程
for (int i = 0; i < 15; i++) {
    executor.submit(() -> {
        System.out.println(Thread.currentThread().getName() + " 执行");
        Thread.sleep(1000);
    });
}
我曾经踩过的坑:有次把 maximumPoolSize 设成 200,核心线程 10,队列容量 100。压测时临时线程数涨到 200,但每个任务只需要几十毫秒,结果临时线程频繁创建销毁,导致 CPU 飙升。后来我改了,把 maximumPoolSize 降到了 20,反而吞吐量上去了。所以你一定要根据任务耗时和 CPU 核心数来估算合理上限,别盲目设大。
线程池扩容决策流程图 任务提交 核心线程空闲? 是 核心线程执行 否 尝试入队 队列已满? 否 等待队列执行 是 线程数 < max? 是 创建非核心线程执行 否 执行拒绝策略 要点:只有队列满且线程数未达 maximumPoolSize 时才创建新线程,否则任务排队或拒绝。

最后总结一下。maximumPoolSize 不是越大越好,也不是越小越好。它跟队列容量、任务类型、拒绝策略一起构成了线程池的「弹性边界」。我建议你在压测时多做两组对比:一组用 SynchronousQueue + 中等 max,另一组用有界队列 + 合理 max,看看哪个能让你的系统在突发流量下保持稳定。