maximumPoolSize 参数详解与最佳实践
先问个问题:你知道线程池到底能创建多少个线程吗?很多人脱口而出「corePoolSize」,其实真正决定上限的是 maximumPoolSize。我见过不少线上事故,就是因为对这个参数理解不透,导致了线程暴增甚至 OOM。
说白了,corePoolSize 是「常驻编制」,maximumPoolSize 是「临时工上限」。只有编制满了、队列也装不下了,才会拉临时工来干活。嗯,这个临时工是有数量限制的,maximumPoolSize 就是这个硬上限。
maximumPoolSize 的真实含义
官方定义:线程池允许的最大线程数量。一旦线程数达到这个值,就不会再创建新线程了。这时候再有任务来,只能走拒绝策略。
那它跟 corePoolSize 到底什么关系?我习惯用一个比喻:
- corePoolSize:正式员工编制,平时就这么多人在岗
- maximumPoolSize:公司总工位上限,包括正式工和临时工
- 临时工只在业务高峰期被拉来干活,高峰期一过就会被回收(前提是 allowCoreThreadTimeOut 或者线程空闲时间超过 keepAliveTime)
你看,这两个参数并不是「越大越好」,而是要根据业务负载和资源来平衡。我曾经在一个项目里把 maximumPoolSize 设得跟 corePoolSize 一样大,结果压测时任务全部排队,延迟飙升——那就是把最大线程数设死了,白白浪费了扩容能力。
扩容时机:从核心到最大
很多新手以为「任务来了就先开线程,直到最大线程数,然后再排队」。错!流程其实是:
- 任务提交,先看当前线程数是否小于 corePoolSize?是则创建核心线程执行
- 如果核心线程已满,任务尝试放入阻塞队列
- 队列满了,才尝试创建新线程——这时的线程数不能超过 maximumPoolSize
- 如果队列满了且线程数已达 maximumPoolSize,执行拒绝策略
我刚开始学的时候也觉得这个顺序很奇怪。为什么要先填队列再扩容?你想想看,CPU 密集型的任务,线程多了反而上下文切换更慢,所以先让任务在队列里排队才是合理的。只有队列真的塞不下了,才说明负载已经超出预期,这时候再扩线程来分担。
| 条件 | 线程数变化 | 任务去向 |
|---|---|---|
| < corePoolSize | 创建核心线程 | 立刻执行 |
| ≥ corePoolSize | 不创建 | 入队等待 |
| 队列已满 & 线程数 < 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 不是越大越好,也不是越小越好。它跟队列容量、任务类型、拒绝策略一起构成了线程池的「弹性边界」。我建议你在压测时多做两组对比:一组用 SynchronousQueue + 中等 max,另一组用有界队列 + 合理 max,看看哪个能让你的系统在突发流量下保持稳定。