1. 概述

Spring 的 ThreadPoolTaskExecutor 是一个 JavaBean,它封装了 java.util.concurrent.ThreadPoolExecutor 实例,并将其暴露为 Spring 的 TaskExecutor 接口。这个类非常灵活,可以通过设置 corePoolSizemaxPoolSizequeueCapacityallowCoreThreadTimeOutkeepAliveSeconds 等属性进行深度定制。

本文重点剖析两个核心参数:corePoolSizemaxPoolSize。这两个值看似简单,但在实际使用中稍不注意就会踩坑,尤其在高并发场景下直接影响系统稳定性。

2. corePoolSize vs. maxPoolSize

刚接触线程池的开发者很容易搞混这两个配置项。我们来逐个拆解它们的含义和行为。

2.1 corePoolSize

corePoolSize 表示线程池中始终保持存活的最小线程数量

  • 这些“核心线程”即使空闲也不会被回收(除非启用了 allowCoreThreadTimeOut=true)。
  • 它是线程池的“基本盘”,用于处理日常负载。
  • 当提交新任务时,只要当前线程数小于 corePoolSize,就会创建新线程来执行任务,哪怕此时有空闲线程(这点很多人误解)。

⚠️ 注意:如果设置了 allowCoreThreadTimeOut = true,那么核心线程也会超时销毁,相当于把 corePoolSize 的下限降到了 0。

2.2 maxPoolSize

maxPoolSize 表示线程池允许创建的最大线程数

  • 它是线程池的“弹性上限”,应对突发流量。
  • 只有当任务队列已满(达到 queueCapacity)且当前线程数小于 maxPoolSize 时,才会创建新的非核心线程。
  • ⚠️ 关键点:maxPoolSize 是否生效,严重依赖 queueCapacity 的设置。如果队列无界(默认行为),那 maxPoolSize 实际上永远不会触发!

3. 核心差异总结

对比项 corePoolSize maxPoolSize
角色 基础容量,常驻线程 弹性扩容上限
创建时机 初始任务提交时,线程不足 队列满 + 任务继续涌入
是否受队列影响 ❌ 不受影响 ✅ 强依赖 queueCapacity
默认值 1 Integer.MAX_VALUE

📌 一句话总结

corePoolSize 决定了“平时养多少人”,maxPoolSize 决定了“忙时最多能叫来多少外援”,而 queueCapacity 则是“任务能排队多长”——三者协同决定线程池的伸缩行为。

4. 代码示例

下面通过几个测试用例直观展示参数行为。假设我们有如下辅助方法用于提交任务:

public void startThreads(ThreadPoolTaskExecutor taskExecutor, CountDownLatch countDownLatch, 
  int numThreads) {
    for (int i = 0; i < numThreads; i++) {
        taskExecutor.execute(() -> {
            try {
                Thread.sleep(100L * ThreadLocalRandom.current().nextLong(1, 10));
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
}

4.1 默认配置:仅一个核心线程

默认情况下,corePoolSize=1maxPoolSize=Integer.MAX_VALUEqueueCapacity=Integer.MAX_VALUE(无界队列)。

@Test
public void whenUsingDefaults_thenSingleThread() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.afterPropertiesSet(); // 必须调用,否则配置不生效

    CountDownLatch countDownLatch = new CountDownLatch(10);
    this.startThreads(taskExecutor, countDownLatch, 10);

    while (countDownLatch.getCount() > 0) {
        Assert.assertEquals(1, taskExecutor.getPoolSize());
    }
}

✅ 结果:无论提交多少任务,线程池始终只有 1 个线程在运行。因为队列无界,所有额外任务都排队了,根本不会触发扩容。


4.2 设置 corePoolSize=5

我们将核心线程数设为 5,观察是否能稳定维持 5 个线程。

@Test
public void whenCorePoolSizeFive_thenFiveThreads() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(5);
    taskExecutor.afterPropertiesSet();

    CountDownLatch countDownLatch = new CountDownLatch(10);
    this.startThreads(taskExecutor, countDownLatch, 10);

    while (countDownLatch.getCount() > 0) {
        Assert.assertEquals(5, taskExecutor.getPoolSize());
    }
}

✅ 结果:线程池稳定在 5 个线程。说明 corePoolSize 起作用了。


4.3 corePoolSize=5, maxPoolSize=10,但 queueCapacity 无界

@Test
public void whenCorePoolSizeFiveAndMaxPoolSizeTen_thenFiveThreads() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(5);
    taskExecutor.setMaxPoolSize(10);
    taskExecutor.afterPropertiesSet(); // queueCapacity 默认无界

    CountDownLatch countDownLatch = new CountDownLatch(10);
    this.startThreads(taskExecutor, countDownLatch, 10);

    while (countDownLatch.getCount() > 0) {
        Assert.assertEquals(5, taskExecutor.getPoolSize());
    }
}

✅ 结果:仍然是 5 个线程。
❌ 原因:queueCapacity 无界,任务全进队列了,压根没触发扩容条件。


4.4 corePoolSize=5, maxPoolSize=10, queueCapacity=10

现在我们把队列容量限制为 10,并提交 20 个任务。

@Test
public void whenCorePoolSizeFiveAndMaxPoolSizeTenAndQueueCapacityTen_thenTenThreads() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(5);
    taskExecutor.setMaxPoolSize(10);
    taskExecutor.setQueueCapacity(10);
    taskExecutor.afterPropertiesSet();

    CountDownLatch countDownLatch = new CountDownLatch(20);
    this.startThreads(taskExecutor, countDownLatch, 20);

    while (countDownLatch.getCount() > 0) {
        Assert.assertEquals(10, taskExecutor.getPoolSize());
    }
}

✅ 结果:线程池最终扩展到了 10 个线程。
📌 过程解析:

  1. 前 5 个任务 → 创建 5 个核心线程
  2. 接下来 10 个任务 → 进队列(队列满)
  3. 剩余 5 个任务 → 队列已满,且线程数 < maxPoolSize → 创建 5 个非核心线程

完美验证了线程池的扩容机制。


4.5 特殊情况:queueCapacity=0

如果设置 queueCapacity=0,意味着不使用队列缓冲,每个任务都尝试直接开线程处理。

// 示例配置
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(0);

此时只要提交的任务数 ≤ 10,就会有多少任务开多少线程(最多到 maxPoolSize)。这种配置适合低延迟、短任务场景,但对系统资源压力大,需谨慎使用。

5. 总结与最佳实践

ThreadPoolTaskExecutor 虽然封装了 JDK 线程池,但如果不理解底层原理,很容易配置出“形同虚设”的线程池。

关键结论

  • corePoolSize 控制基础并发能力
  • maxPoolSize 只有在 queueCapacity 有限时才可能生效
  • 无界队列 + 默认配置 = 线程池失去弹性,容易 OOM

生产建议

  • 避免使用无界队列(queueCapacity=Integer.MAX_VALUE),防止内存溢出
  • 根据业务峰值合理设置 maxPoolSize,并配合 RejectedExecutionHandler
  • 对于实时性要求高的接口,可设置 queueCapacity=0,但必须严格控制 maxPoolSize
  • 监控 getPoolSize()getActiveCount()getQueue().size() 等指标,及时发现配置问题

所有示例代码已托管至 GitHub:https://github.com/spring-threads-demo
建议动手跑一遍测试用例,印象更深刻。


原始标题:ThreadPoolTaskExecutor corePoolSize vs. maxPoolSize