Java线程池参数配置的坑:`corePoolSize=0` + `LinkedBlockingQueue`直接变成串行执行
问题描述
在某项目代码中发现了很多这样的线程池的配置:
this.executorService =new ThreadPoolExecutor(0,10,60,TimeUnit.SECONDS,new LinkedBlockingQueue<>(),new ThreadFactoryBuilder().setNameFormat("Xxxx-%d").build());
其 corePoolSize 设置为0,而阻塞队列选择了 LinkedBlockingQueue,这是一个无界队列,默认大小是Integer.MAX_VALUE 2³¹-1(约21亿)。
这个配置的出发点是为了线程能够仅在需要时创建,从而避免没有业务请求的时候占用系统资源。只能说出发点是好的,但是问题就出在没有深刻理解线程池的线程创建机制,我们参考Java线程池实现原理及其在美团业务中的实践中的任务调度流程图:
可以看到ThreadPoolExecutor的三大执行规则:
- 规则1:workerCount < corePoolSize ? 创建核心线程。
- 规则2:workQueue.offer() ?任务入队。
- 规则3:workerCount < maximumPoolSize ? 创建临时线程。
这里还有一个补充规则 零工作线程保护规则:在一个任务被成功放入队列之后,线程池会再做一次检查。如果此时发现线程池里一个工作线程都没有 (workerCount == 0),它会创建一个新的线程来确保队列里的任务能被及时处理。
现在,把新配置代入任务提交流程:
- 开始提交5000个任务。
- 提交第1个任务:
- 规则1 (
0 < 0
) 不满足。 - 规则2 (
workQueue.offer()
) 成功。因为LinkedBlockingQueue
是无界的,这个操作永远不会失败。任务1进入队列。 - 零工作线程保护规则被触发:检查发现
workerCount
为0,于是创建一个新线程(Thread-1)。Thread-1启动后,从队列中取出任务1开始执行。
- 规则1 (
- 提交第2个任务 到 第5000个任务:
- 规则1 (
1 < 0
) 不满足。 - 规则2 (
workQueue.offer()
) 永远成功。任务2、3、4…5000被源源不断地放入无界队列。 - 补充规则不被触发,因为
workerCount
一直为1。
- 规则1 (
- 规则3永远不会被触发!
- 因为规则2(任务入队)永远不会失败,所以程序流程永远没有机会进入到规则3。
- 这意味着,
maximumPoolSize
这个参数在这种配置下完全失效了,形同虚设。线程池永远不会创建超过corePoolSize
(在这里是1,因为零工作线程保护规则创建了一个)个线程。
结论:灾难性的结果
如果使用 corePoolSize=0
+ LinkedBlockingQueue
的组合,系统将表现为:
无论提交多少任务,永远只有一个线程在工作。
由于上述配置中队列是无界的,任务会无限堆积在队列中,永远没有机会去触发“创建临时线程”的逻辑,maximumPoolSize也就成了摆设。
验证程序
下面是一个验证程序,可以通过手动更改其 corePoolSize 或 LinkedBlockingQueue 的大小来验证程序的执行情况:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class Test {public static void main(String[] args) throws InterruptedException {// --- 1. 定义测试参数 ---final int TASK_COUNT = 20; // 提交的任务数量,特意设置为 maximumPoolSize 的两倍final int TASK_SLEEP_SECONDS = 1; // 每个任务的耗时(秒)// --- 2. 创建有问题的线程池配置 ---// 这是我们要测试的核心配置int corePoolSize = 0;int maximumPoolSize = 10;long keepAliveTime = 60;TimeUnit unit = TimeUnit.SECONDS;BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 无界队列是关键// 自定义线程工厂,方便观察线程名ThreadFactory threadFactory = new ThreadFactory() {private final AtomicInteger threadNumber = new AtomicInteger(1);@Overridepublic Thread newThread(Runnable r) {return new Thread(r, "misconfigured-pool-thread-" + threadNumber.getAndIncrement());}};ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory);// --- 3. 打印测试说明 ---System.out.println("--- 线程池行为测试 ---");System.out.println("配置: corePoolSize=0, maximumPoolSize=10, workQueue=LinkedBlockingQueue (无界)");System.out.println("提交任务数: " + TASK_COUNT);System.out.println("每个任务耗时: " + TASK_SLEEP_SECONDS + " 秒");System.out.println("------------------------------------");System.out.println("预期结果分析:");System.out.println(" - 如果是10线程并行: 理想总耗时应约为 " +(TASK_COUNT / maximumPoolSize * TASK_SLEEP_SECONDS) + " 秒 (20任务/10线程 * 1秒)。");System.out.println(" - 如果是单线程串行: 理想总耗时应约为 " +(TASK_COUNT * TASK_SLEEP_SECONDS) + " 秒 (20任务 * 1秒)。");System.out.println("------------------------------------");System.out.println("测试开始,正在提交任务...");// --- 4. 执行测试 ---long startTime = System.currentTimeMillis();for (int i = 1; i <= TASK_COUNT; i++) {final int taskNumber = i;executor.submit(() -> {System.out.printf("任务 %2d 开始执行... | 由线程: %s%n",taskNumber, Thread.currentThread().getName());try {TimeUnit.SECONDS.sleep(TASK_SLEEP_SECONDS);} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.printf("任务 %2d 执行完毕。 | 队列中剩余任务数: %d%n",taskNumber, executor.getQueue().size());});}// --- 5. 关闭线程池并等待所有任务完成 ---executor.shutdown();// 等待足够长的时间以确保所有任务都执行完毕boolean finished = executor.awaitTermination(TASK_COUNT * TASK_SLEEP_SECONDS + 5, TimeUnit.SECONDS);long endTime = System.currentTimeMillis();long totalDurationSeconds = TimeUnit.MILLISECONDS.toSeconds(endTime - startTime);// --- 6. 打印并分析测试结果 ---System.out.println("------------------------------------");System.out.println("测试结束。所有任务已执行完毕。");System.out.println("实际总耗时: " + totalDurationSeconds + " 秒");System.out.println("------------------------------------");System.out.println("结论分析:");if (totalDurationSeconds >= TASK_COUNT) {System.out.println("✅ 实验结果符合预期:线程池表现为【单线程串行执行】。");System.out.println("原因:由于使用了无界的 LinkedBlockingQueue,任务会无限进入队列,");System.out.println("导致永远无法触发创建新线程(直到 maximumPoolSize)的逻辑。");System.out.println("maximumPoolSize=10 和 keepAliveTime=60 参数在此配置下完全失效。");} else {System.out.println("❌ 实验结果与理论不符,请检查测试环境或代码。");}}
}
实测结果
--- 线程池行为测试 ---
配置: corePoolSize=0, maximumPoolSize=10, workQueue=LinkedBlockingQueue (无界)
提交任务数: 20
每个任务耗时: 1 秒
------------------------------------
预期结果分析:- 如果是10线程并行: 理想总耗时应约为 2 秒 (20任务/10线程 * 1秒)。- 如果是单线程串行: 理想总耗时应约为 20 秒 (20任务 * 1秒)。
------------------------------------
测试开始,正在提交任务...
任务 1 开始执行... | 由线程: misconfigured-pool-thread-1
任务 1 执行完毕。 | 队列中剩余任务数: 19
任务 2 开始执行... | 由线程: misconfigured-pool-thread-1
任务 2 执行完毕。 | 队列中剩余任务数: 18
任务 3 开始执行... | 由线程: misconfigured-pool-thread-1
任务 3 执行完毕。 | 队列中剩余任务数: 17
任务 4 开始执行... | 由线程: misconfigured-pool-thread-1
任务 4 执行完毕。 | 队列中剩余任务数: 16
任务 5 开始执行... | 由线程: misconfigured-pool-thread-1
任务 5 执行完毕。 | 队列中剩余任务数: 15
任务 6 开始执行... | 由线程: misconfigured-pool-thread-1
任务 6 执行完毕。 | 队列中剩余任务数: 14
任务 7 开始执行... | 由线程: misconfigured-pool-thread-1
任务 7 执行完毕。 | 队列中剩余任务数: 13
任务 8 开始执行... | 由线程: misconfigured-pool-thread-1
任务 8 执行完毕。 | 队列中剩余任务数: 12
任务 9 开始执行... | 由线程: misconfigured-pool-thread-1
任务 9 执行完毕。 | 队列中剩余任务数: 11
任务 10 开始执行... | 由线程: misconfigured-pool-thread-1
任务 10 执行完毕。 | 队列中剩余任务数: 10
任务 11 开始执行... | 由线程: misconfigured-pool-thread-1
任务 11 执行完毕。 | 队列中剩余任务数: 9
任务 12 开始执行... | 由线程: misconfigured-pool-thread-1
任务 12 执行完毕。 | 队列中剩余任务数: 8
任务 13 开始执行... | 由线程: misconfigured-pool-thread-1
任务 13 执行完毕。 | 队列中剩余任务数: 7
任务 14 开始执行... | 由线程: misconfigured-pool-thread-1
任务 14 执行完毕。 | 队列中剩余任务数: 6
任务 15 开始执行... | 由线程: misconfigured-pool-thread-1
任务 15 执行完毕。 | 队列中剩余任务数: 5
任务 16 开始执行... | 由线程: misconfigured-pool-thread-1
任务 16 执行完毕。 | 队列中剩余任务数: 4
任务 17 开始执行... | 由线程: misconfigured-pool-thread-1
任务 17 执行完毕。 | 队列中剩余任务数: 3
任务 18 开始执行... | 由线程: misconfigured-pool-thread-1
任务 18 执行完毕。 | 队列中剩余任务数: 2
任务 19 开始执行... | 由线程: misconfigured-pool-thread-1
任务 19 执行完毕。 | 队列中剩余任务数: 1
任务 20 开始执行... | 由线程: misconfigured-pool-thread-1
任务 20 执行完毕。 | 队列中剩余任务数: 0
------------------------------------
测试结束。所有任务已执行完毕。
实际总耗时: 20 秒
------------------------------------
结论分析:
✅ 实验结果符合预期:线程池表现为【单线程串行执行】。
原因:由于使用了无界的 LinkedBlockingQueue,任务会无限进入队列,
导致永远无法触发创建新线程(直到 maximumPoolSize)的逻辑。
maximumPoolSize=10 和 keepAliveTime=60 参数在此配置下完全失效。
如果将LinkedBlockingQueue换为大小为10的队列:new LinkedBlockingQueue<>(10),那么表现如下:
测试开始,正在提交任务...
任务 1 开始执行... | 由线程: misconfigured-pool-thread-1
任务 20 开始执行... | 由线程: misconfigured-pool-thread-10
任务 19 开始执行... | 由线程: misconfigured-pool-thread-9
任务 18 开始执行... | 由线程: misconfigured-pool-thread-8
任务 17 开始执行... | 由线程: misconfigured-pool-thread-7
任务 16 开始执行... | 由线程: misconfigured-pool-thread-6
任务 15 开始执行... | 由线程: misconfigured-pool-thread-5
任务 14 开始执行... | 由线程: misconfigured-pool-thread-4
任务 13 开始执行... | 由线程: misconfigured-pool-thread-3
任务 11 开始执行... | 由线程: misconfigured-pool-thread-2
任务 1 执行完毕。 | 队列中剩余任务数: 10
任务 20 执行完毕。 | 队列中剩余任务数: 10
任务 19 执行完毕。 | 队列中剩余任务数: 9
任务 4 开始执行... | 由线程: misconfigured-pool-thread-9
任务 18 执行完毕。 | 队列中剩余任务数: 7
任务 5 开始执行... | 由线程: misconfigured-pool-thread-8
任务 2 开始执行... | 由线程: misconfigured-pool-thread-1
任务 15 执行完毕。 | 队列中剩余任务数: 6
任务 16 执行完毕。 | 队列中剩余任务数: 6
任务 17 执行完毕。 | 队列中剩余任务数: 7
任务 11 执行完毕。 | 队列中剩余任务数: 4
任务 9 开始执行... | 由线程: misconfigured-pool-thread-2
任务 3 开始执行... | 由线程: misconfigured-pool-thread-10
任务 8 开始执行... | 由线程: misconfigured-pool-thread-7
任务 7 开始执行... | 由线程: misconfigured-pool-thread-6
任务 13 执行完毕。 | 队列中剩余任务数: 5
任务 10 开始执行... | 由线程: misconfigured-pool-thread-3
任务 6 开始执行... | 由线程: misconfigured-pool-thread-5
任务 14 执行完毕。 | 队列中剩余任务数: 6
任务 12 开始执行... | 由线程: misconfigured-pool-thread-4
任务 4 执行完毕。 | 队列中剩余任务数: 0
任务 5 执行完毕。 | 队列中剩余任务数: 0
任务 2 执行完毕。 | 队列中剩余任务数: 0
任务 9 执行完毕。 | 队列中剩余任务数: 0
任务 3 执行完毕。 | 队列中剩余任务数: 0
任务 8 执行完毕。 | 队列中剩余任务数: 0
任务 7 执行完毕。 | 队列中剩余任务数: 0
任务 10 执行完毕。 | 队列中剩余任务数: 0
任务 6 执行完毕。 | 队列中剩余任务数: 0
任务 12 执行完毕。 | 队列中剩余任务数: 0
------------------------------------
测试结束。所有任务已执行完毕。
实际总耗时: 2 秒
------------------------------------
执行流程推演
初始状态:core=0
, max=10
, queue capacity=10
, workerCount=0
。
-
main
线程提交Task 1
:execute(task1)
被调用。- 规则1 (
0 < 0
) 不满足。 - 规则2 (
queue.offer(task1)
) 成功。此刻,Task 1
进入了队列。队列大小为1。 - “补充规则”触发!发现
workerCount=0
,于是开始创建Thread-1
。注意,只是“开始创建”,它还没启动完成。
-
main
线程继续提交Task 2
到Task 10
(共9个任务):main
线程的速度极快,它在Thread-1
还在“热身”的时候,就一口气把Task 2
到Task 10
全部提交了。- 对于这9个任务,每次检查规则1 (
workerCount < 0
) 都不满足。 - 规则2 (
queue.offer()
) 每次都成功。 - 此刻状态:
Task 1
到Task 10
共10个任务,全部躺在了队列里。队列已满!workerCount
可能仍然是1(因为Thread-1
正在创建中)。
-
引爆点!
main
线程提交Task 11
:execute(task11)
被调用。- 规则1 (
workerCount < 0
) 不满足。 - 规则2 (
queue.offer(task11)
) 失败! 因为队列在第2步中已经被填满了。 - 流程进入规则3!
- 检查
workerCount < max
(1 < 10
)?满足! - 线程池决定为
Task 11
创建一个新的救急线程。这个新线程就是Thread-2
。Thread-2
被创建时,是直接绑定了Task 11
的!
-
main
线程继续提交Task 12
到Task 20
:- 这个过程和第3步完全一样。
submit(task12)
-> 队列满 -> 创建Thread-3
执行Task 12
。- …
submit(task20)
-> 队列满 -> 创建Thread-10
执行Task 20
。