线程池明明还有最大线程闲着,为什么任务非要进队列排队?

📅 2026/7/17 10:31:56
线程池明明还有最大线程闲着,为什么任务非要进队列排队?
很多人刚接触 Java 线程池ThreadPoolExecutor的时候有过这样的疑问嘛就是我把核心线程数corePoolSize设为 5最大线程数maximumPoolSize设为 10任务队列容量设为 100。按照常理来说如果有 6 个并发任务进来了核心线程满了那线程池是不是应该赶紧创建第 6 个线程早点把任务做完实现上处理的根本不这样第 6 个任务并没有触发新线程的创建而是进队列排队去了。只有当这 100 个队列位置全部塞满第 106 个任务进来时线程池才会去创建第 6 个线程非核心线程。不理解的是我有闲置的算力最大线程 10为什么非要让任务在队列里卡着不立刻新建线程去干活呢线程池是怎么跑任务的为了搞清楚这个问题我们先简单看看 JDK 官方对线程池的执行逻辑是怎么设定的。当一个任务丢给线程池它的处理逻辑其实很简单就四步核心线程没满没得说直接开个新线程去跑任务哪怕有其他核心线程闲着也新建。核心线程满了那就把任务塞进队列里让它排队等着。队列也塞满了这时候如果线程数还没到最大线程数那就再开个临时线程来跑任务。队列满了最大线程数也到了那就执行拒绝策略。为什么是先队列后最大线程线程池的本质其实是限流和缓冲不是一上来就无脑拉满 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 默认逻辑配合一个合理大小的有界队列才是最稳妥的方案。