今天去吃烤鱼,排队的时候我突然陷入了思考。
场景复现:这家店生意很火。
- 店里原本有 10 张桌子(核心线程),全坐满了。
- 老板看到还有人来,立马把门口露天的 5 张折叠桌也支棱起来了(最大线程)。
- 折叠桌也坐满了,老板才开始发号,让大家在门口坐小板凳排队(队列)。
- 排队的人实在太多,老板只能出来挥手:"别排了,今天的鱼卖完了"(拒绝策略)。
这逻辑很顺吧?
先尽全力接待(扩容),实在不行了再让人等(排队)。
可我转念一想------
Java 原生线程池(ThreadPoolExecutor)的逻辑怎么是反着来的?
它的流程是:
核心线程满 → 进队列排队 → 队列满了才去扩容(创建最大线程)
这就好比:店里 10 张桌子坐满了,老板不先支折叠桌,而是非要让客人在门口排队。
直到门口小板凳都坐不下了,才不情不愿地去支折叠桌。
这不是反人类吗?
回来后我翻了源码,终于想通了其中的博弈。
1. 核心矛盾:资源视角的差异
烤鱼店视角(IO 密集型 / 响应优先)
- 目标:赚钱,不让客人跑了
- 成本:折叠桌(线程)很便宜,客人等久了会走
- 策略:保响应,只要有空地,赶紧把桌子支起来,别让客人干等
Java JDK 视角(计算密集型 / 资源优先)
- 目标:保护 CPU,防止系统崩盘
- 成本:线程是昂贵资源
- 创建一个线程默认占 1MB 栈空间(栈)
- 线程太多,上下文切换会把 CPU 拖死
- 队列是廉价资源:一个任务对象扔堆里,几乎不花钱
- 策略:省资源,只要核心线程能扛,就绝不轻易加人
"先让任务在内存里排会儿队吧,实在扛不住我再加人。"
2. 破局:Tomcat 的"烤鱼流"线程池
难道 Java 就不能像烤鱼店一样工作吗?
能,而且 Tomcat/Jetty 就是这么干的。
Web 请求是典型的 IO 密集型任务(读 DB、调接口,CPU 经常空闲)。
如果按 JDK 默认逻辑(先排队),用户在浏览器前就等到死。
Tomcat 的骚操作:
它继承了 ThreadPoolExecutor,但魔改了队列(TaskQueue)。
- 核心线程满了,任务想入队时,队列会伪造"我已满"的假象(offer 返回 false)
- 线程池一听"队列满了",立刻创建新线程(支折叠桌)
- 只有当线程数真的达到 maximumPoolSize,队列才会真正接收任务开始排队
3. JDK 默认线程池的世纪大坑
初学者喜欢这么写:
java
Executors.newFixedThreadPool(10)
展开其实是:
java
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); // ⚠️ 坑在这里
}
LinkedBlockingQueue 是无界队列(默认 Integer.MAX_VALUE)
后果:
- 队列永远装不满 →
maximumPoolSize参数直接失效(永远轮不到它出场) - 任务处理不过来时,队列无限膨胀 → OOM → 服务直接挂掉
这才是真正的生产事故之王。
4. 总结与避坑指南
两种逻辑对比:
| 场景 | 执行顺序 | 适用场景 |
|---|---|---|
| JDK 默认 | 核心 → 队列 → 最大 → 拒绝 | 后台计算任务、保护 CPU |
| 烤鱼店/Tomcat | 核心 → 最大 → 队列 → 拒绝 | 高并发 Web、追求低延迟 |
生产环境铁律:
- 禁止使用
Executors的快捷方法创建线程池 - 必须手动
new ThreadPoolExecutor(...) - 必须用有界队列(推荐
ArrayBlockingQueue) - 拒绝策略推荐
CallerRunsPolicy(让调用者线程自己干活,实现天然背压)
下次你去吃烤鱼排队时,注意看老板是先支折叠桌,还是先让你们排队。
如果他先支折叠桌------
恭喜你,这老板懂高并发,比很多写 Java 的都强。