今天咱们不聊那些深入的话题,来聊一个很多兄弟在面试或者实际调优时都会懵圈的问题:
为什么 Java 原生线程池(ThreadPoolExecutor)是“核心线程满 -> 排队 -> 队列满 -> 才创建最大线程”?
而 Tomcat 的线程池却是“核心线程满 -> 直接创建最大线程 -> 队列满 -> 才拒绝”?
这俩是不是搞反了?到底谁才是对的?别急,咱们搬个小板凳,用大白话把这事儿捋清楚。
Java 原生线程池:先排队,后加人
首先,咱们看看 JDK 自带的 ThreadPoolExecutor 的逻辑。它的执行流程大概是这样的:
- 来个任务,核心线程数(corePoolSize)没满?直接新建线程干活。
- 核心线程满了?别急着加人,先去队列(workQueue)里排着。
- 队列也满了?这时候才考虑创建非核心线程,直到达到最大线程数(maximumPoolSize) 。
- 最大线程数也满了?那就对不起了,触发拒绝策略。
很多初学者的第一反应是: “这不合理啊!队列都堆积如山了,说明忙不过来了,为什么不赶紧多招人(创建新线程)来帮忙,反而让任务在那干等着?”
其实,JDK 的设计者这么定,核心逻辑就两个字:资源。
保护系统,防止“虚胖”
想象一下,你是一个包工头(CPU)。核心线程是你手下的正式员工,能力强,长期雇佣。最大线程是你临时去劳务市场找的兼职,成本高,管理麻烦。队列是门口的等候区。
如果按照“先加人”的逻辑:只要正式员工忙不过来,你就疯狂招兼职。万一只是瞬间来了一个小高峰(比如突发流量),你招了一堆兼职,结果人家刚到位,流量没了。这一堆兼职线程空转,不仅浪费 CPU 上下文切换的资源,还占内存。
JDK 的逻辑是: 正式员工忙了,让任务在门口(队列)等一会儿。如果只是瞬间高峰,等几秒钟,正式员工手头活干完了,立马就能把门口的活接了,根本不需要招兼职。
只有当门口(队列)真的堵死了,说明这波流量不是暂时的,是持续的洪峰,这时候再招兼职(最大线程)也不迟。
2. 适用场景:IO 密集型 vs 计算密集型
JDK 这种设计,特别适合任务执行时间较短、且希望控制资源消耗的场景。它倾向于用“空间(队列内存)换时间(线程创建开销)”,优先保证系统的稳定性,避免线程数爆炸。
Tomcat 线程池:先加人,后排队
好了,再看 Tomcat。如果你去翻 Tomcat 的源码或者配置,你会发现它的逻辑完全是反过来的:
- 来个请求,核心线程没满?新建线程。
- 核心线程满了?别排队!直接创建新线程,直到达到最大线程数。
- 最大线程数也满了?这时候才让请求去队列(AcceptCount)里排队。
- 队列也满了?拒绝连接。
这就奇怪了,Tomcat 为啥跟 JDK 对着干?
Web 服务器的特殊性:快速响应
Tomcat 是干嘛的?它是处理 HTTP 请求的。Web 请求有一个特点:用户是在线等待的。
如果用 JDK 那套逻辑:
- 用户发起请求。
- 核心线程满了。
- 用户被扔进队列排队。
- 注意: 在 Java 原生线程池里,如果队列是无界的(比如
LinkedBlockingQueue),或者队列很大,任务可能会在队列里躺很久,直到核心线程空闲。 - 对于用户来说,这就是接口超时、页面转圈圈、甚至直接报错。
Tomcat 的逻辑是:能多开一个线程处理,就绝不让用户排队!
2. 避免“队头阻塞”效应
在 Web 场景下,如果大量请求进入队列,而处理速度跟不上,队列会迅速变长。
- JDK 模式风险:请求在队列里积压,响应时间不可控。哪怕后面有闲置的“最大线程”名额,但因为队列没满,这些名额根本不会启动。这就造成了资源闲置(有名额不用)和用户体验差(用户在排队)并存的尴尬局面。
- Tomcat 模式优势:只要还没达到最大线程上限,立刻新开线程处理请求。这样能最大程度利用服务器资源,降低平均响应时间(RT) 。只有当服务器真的扛不住了(线程数达到最大值),才让后续请求排队(此时通常意味着系统已经过载,排队也是一种保护)。
3. 这里的“队列”含义不同
还要区分一下概念:
- JDK 线程池的队列:存的是
Runnable任务对象,是在内存里的。 - Tomcat 的队列(AcceptCount) :在达到最大线程数后,新的 TCP 连接会在操作系统内核的监听队列里排队,或者在 Tomcat 的应用层队列等待。这时候客户端通常会表现为“连接建立中”或者超时。Tomcat 宁愿先耗尽线程资源,也不愿过早让连接进入应用层排队,因为线程是可以并发处理的,而队列是串行的等待。
说到这,大家应该明白了。这俩都不是“反着来”,而是针对不同的业务场景做的最优解。
更多内容请关注我的公众号
简迅云笔记
推荐一个 ai 工具网站 码盒工具