线程池明明还有最大线程闲着,为什么任务非要进队列排队?
2026/7/17 10:31:52 网站建设 项目流程

很多人刚接触 Java 线程池ThreadPoolExecutor的时候,有过这样的疑问嘛

就是我把核心线程数(corePoolSize)设为 5,最大线程数(maximumPoolSize)设为 10,任务队列容量设为 100。

按照常理来说,如果有 6 个并发任务进来了,核心线程满了,那线程池是不是应该赶紧创建第 6 个线程,早点把任务做完?

实现上处理的根本不这样!第 6 个任务并没有触发新线程的创建,而是进队列排队去了。只有当这 100 个队列位置全部塞满,第 106 个任务进来时,线程池才会去创建第 6 个线程(非核心线程)。

不理解的是:我有闲置的算力(最大线程 10),为什么非要让任务在队列里卡着,不立刻新建线程去干活呢?

线程池是怎么跑任务的?

为了搞清楚这个问题,我们先简单看看 JDK 官方对线程池的执行逻辑是怎么设定的。

当一个任务丢给线程池,它的处理逻辑其实很简单,就四步:

  1. 核心线程没满:没得说,直接开个新线程去跑任务,哪怕有其他核心线程闲着也新建。

  2. 核心线程满了:那就把任务塞进队列里,让它排队等着。

  3. 队列也塞满了:这时候如果线程数还没到最大线程数,那就再开个临时线程来跑任务。

  4. 队列满了,最大线程数也到了:那就执行拒绝策略。

为什么是先队列,后最大线程?

线程池的本质其实是限流和缓冲,不是一上来就无脑拉满 CPU。

想想如果把逻辑反过来:核心线程满了 -> 立刻创建最大线程 -> 最大线程满了再进队列排队,会出现什么情况?

新建线程是非常重的操作。Java 新建一个线程不仅要在 JVM 层面分配内存,默认一个线程栈就是 1MB,还得通过系统调用在操作系统内核创建对应的物理线程。如果瞬间涌入上千个任务,线程池直接创建上千个线程,会引发 CPU 剧烈抖动,重则导致内存溢出 OOM。

线程多并不等于处理速度快。CPU 的核心数是有限的,几百个线程同时争抢几个 CPU 核心,系统会频繁触发上下文切换,大量CPU时间都浪费在保存和恢复线程现场上,真正用来跑业务代码的时间反而变少了,这就是为什么线程多处理的性能反而会变慢了。

所以用一个队列在中间做缓冲,是非常明智的手段。

类比一下

这种设计其实生活中很多地方都有应用,你看银行办业务:银行大堂里平时就开 3 个常规窗口(对应核心线程)。这时候来了第 4 个办业务的顾客,大堂经理肯定不会立刻从后台叫个值班经理出来,为他单独新开一个临时窗口。相反经理会指指旁边的长椅(对应工作队列),先去那坐着排个号。

只有当长椅坐满了(队列满了),大堂里人多了 ,眼看就要挤爆了,大堂经理才会赶紧让后台的值班人员出来,增开几个临时窗口(对应最大线程)加紧处理。

如果连个站脚的地方都没了(最大线程也满了,队列也爆了),那保安只能在门口挂牌子:今日业务已满,请明天再来(对应拒绝策略)。

通过这种先排队,实在不行再开临时窗口的设计,线程池能够以很小的资源开销,应对瞬时的大流量。

能不能先起最大线程,再去排队?

JDK 线程池之所以这么设计,核心是为了保护服务器资源,适合跑那种后台任务、批处理等不太在乎响应延迟的场景。

但如果你碰上像Tomcat这样的 Web 服务器,这套逻辑就非常尴尬了。

用户发起一个 HTTP 请求,对响应速度要求极高。如果核心线程满了,请求被丢进队列排队,哪怕这时候最大线程数没满,CPU 也闲着,用户也只能盯着网页一直转圈,用户体验极差。

作为 Web 服务器,我们更希望的是:能多开线程就多开线程,让任务立刻被执行,实在开不出新线程了,再丢进队列排队。

Tomcat 专门定制了自己的一套线程池。

Tomcat 怎么实现先建线程再排队?

Tomcat 并没有重新写一遍ThreadPoolExecutor,是改造了工作队列。JDK 线程池源码里尝试把任务丢进队列的地方:

// JDK ThreadPoolExecutor 源码片段 if (isRunning(c) && workQueue.offer(command)) { // 入队成功,等待核心线程消费 } else if (addWorker(command, false)) { // 入队失败,尝试创建非核心线程(也就是最大线程数以内的线程) }

线程池会不会创建新线程,完全取决于workQueue.offer(command)是否返回false,也就是入队失败。

Tomcat 通过继承LinkedBlockingQueue,实现了一个TaskQueue,并重写了offer方法:

@Override public boolean offer(Runnable o) { // 1. 如果当前线程数等于最大线程数,没得新建了,直接乖乖入队 if (parent.getPoolSize() == parent.getMaximumPoolSize()) { returnsuper.offer(o); } // 2. 如果当前提交的任务数,小于等于当前已有的线程数 // 说明现有的线程够用,不需要新建线程,直接入队 if (parent.getSubmittedCount() <= parent.getPoolSize()) { returnsuper.offer(o); } // 3. 【核心逻辑】如果当前线程数少于最大线程数 // 我们希望线程池去创建新线程,而不是排队! // 所以这里直接返回 false,欺骗线程池“队列已满”,逼迫它去创建新线程! if (parent.getPoolSize() < parent.getMaximumPoolSize()) { returnfalse; } // 默认入队 returnsuper.offer(o); }

通过这一段逻辑,Tomcat 成功实现了:核心线程满 -> 继续创建最大线程 -> 最大线程满 -> 进入队列排队 -> 队列满 -> 触发拒绝。

说在最后

很多开发兄弟在配置线程池参数时,喜欢顺手写个new LinkedBlockingQueue()

别忘了这个无参构造器默认的队列容量是Integer.MAX_VALUE,接近 21 亿,这意味着你的队列几乎永远不可能被塞满。

在这种情况下,你配置的maximumPoolSize最大线程数直接成了摆设,任务全在队列里排队,一旦并发量上来,不仅响应慢,还极容易发生内存溢出。

不要盲目配置线程池,要永远根据实际业务场景来,比如:

如果是高并发、低延迟的接口,可以参考 Tomcat 的思路,定制队列优先创建线程;

如果是后台批处理、高吞吐的业务,用 JDK 默认逻辑配合一个合理大小的有界队列,才是最稳妥的方案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询