ThreadPool 7 个参数怎么配?100 个任务压进去看 4 种拒绝策略
corePoolSize=4、maximumPoolSize=8、queueCapacity=10,100 个任务每 20ms 提交一个,每个跑 250ms。
AbortPolicy 完成 70 个,拒绝 30 个。CallerRunsPolicy 完成 100 个,总耗时从 2520ms 涨到 3310ms。
我把这条派发链路做成了一个能拖时间轴的沙盘。参数一改,worker 泳道、等待队列和拒绝清单会同步变化。

一、先说结论:队列决定线程扩不扩
ThreadPoolExecutor 的 execute 只有四步。
运行数小于 corePoolSize,直接新建核心线程。
否则把任务放进队列。放得进去就返回,线程数停在 core。
队列满了,才去扩到 maximumPoolSize。
max 也满了,最后才交给拒绝策略。
很多人以为任务一多就会加线程。不是。队列没满之前,max 这个参数根本不会生效。
我把 queueCapacity 从 10 改成 2 跑了一遍。AbortPolicy 的拒绝数从 30 涨到 34。
二、为什么值得看:配错的代价不是报错,是静默
线程池配错,最坏的结果往往不是抛异常。
队列太长,任务在里面积压,接口等到超时。
DiscardPolicy 更隐蔽。任务被丢掉,提交方什么都收不到。
CallerRunsPolicy 看起来安全,但它会让提交任务的线程自己干活,上游吞吐会被拖慢。
这四种策略的名字很好记,行为却很容易混。
我把它们放在同一批任务、同一组参数下跑,差异就变成了一张能对照的表。
顺带说一个常见坑:Executors.newFixedThreadPool 用的是无界队列。
队列永远塞得进去,maximumPoolSize 就永远用不上,线程数只会停在 core。
阿里 Java 开发手册不建议直接用 Executors 的工厂方法,原因就在这里。
三、我做了什么:用虚拟时钟复刻派发顺序
页面没有用真实 sleep 去模拟并发。
它维护一个虚拟事件队列,按提交、开始、完成三类事件推进时钟。
同一组参数每次跑出来的数字完全一样。截图和文章里的数据才能对得上。
派发逻辑照着 ThreadPoolExecutor#execute 的顺序写。
if (workerCount < corePoolSize) 新建核心线程;
else if (queue.offer(task)) 入队返回;
else if (workerCount < maximumPoolSize) 新建非核心线程;
else reject(task);
这段代码不是我事后补的说明。页面左下角就贴着同一份逻辑。

四、数据:100 个任务下四种策略的差异
参数固定为 core=4、max=8、queue=10,任务 100 个,每 20ms 提交一个,每个任务 250ms。
| 策略 | 提交 | 完成 | 拒绝 | 丢弃 | 队列峰值 | 最大等待 | 总耗时 | 吞吐量 |
|---|---|---|---|---|---|---|---|---|
| AbortPolicy | 100 | 70 | 30 | 0 | 10 | 430ms | 2520ms | 27.8 t/s |
| CallerRunsPolicy | 100 | 100 | 0 | 0 | 10 | 430ms | 3310ms | 30.2 t/s |
| DiscardPolicy | 100 | 70 | 0 | 30 | 10 | 430ms | 2520ms | 27.8 t/s |
| DiscardOldestPolicy | 100 | 70 | 0 | 30 | 10 | 290ms | 2520ms | 27.8 t/s |
这张表是页面里点一次「运行四种策略对比」直接跑出来的。
AbortPolicy 和 DiscardPolicy 都完成 70 个。区别在溢出处理:一个抛异常,一个静默丢弃。
CallerRunsPolicy 是唯一 100 个全完成的,代价是总耗时多了 790ms。
DiscardOldestPolicy 把最大等待从 430ms 压到 290ms,但丢弃数还是 30。
它优化的是排队时间,不是完成率。

五、验证与踩坑:17 项断言和一个真 bug
页面加载后会自己跑 17 项断言,逐条显示 PASS 和 FAIL,以及实际值。
自检 17/17 通过。
我先加的是三条最容易被忽略的断言。
队列只有 2 格时,AbortPolicy 必须出现拒绝,实际 34 个。
队列设成 9999 时,不会扩容超过 core=4,实际最大 worker=4。
CallerRunsPolicy 必须由 MAIN 线程执行溢出任务,实际执行了 4 个。
然后是那个真 bug。
我试着把 maximumPoolSize 填成 0。
第一版页面没报错,运行按钮还能点。
但 Java 的 ThreadPoolExecutor 构造器会直接抛 IllegalArgumentException,因为 maximumPoolSize 必须大于 0。
我给参数校验补了一条 max 小于 1 的分支。
现在填 0 会变红、禁用运行,并给出「maximumPoolSize (0) 必须 ≥ 1」。

六、本地复现:一个 HTML 文件,起个静态服务就能跑
整个应用就是一个 index.html,没有构建步骤,也没有后端。
把它放进任意目录,执行:
python -m http.server 8080
浏览器打开 http://127.0.0.1:8080/index.html 就能看到。
自检会在页面加载后自动跑,底部能看到每一条的实际值。
想改参数,直接改左边的输入框或拖滑块,再点「运行模拟」。
想一次看四种策略,点「运行四种策略对比」。
七、使用体会:四个数字互相拉扯
做完这个沙盘,我最大的感受是:线程池参数不是「调大一点更安全」。
core 决定常驻兵力,queue 决定缓冲深度,max 决定峰值兵力,拒绝策略决定溢出时谁来兜底。
先把 queue 设小、把拒绝策略显式写出来,再根据压测结果调 core 和 max,比一上来抄「核心数等于 CPU 核数」的公式靠谱。
当然,这个沙盘只是派发逻辑的模型,不是 JMH 压测。
真实 JVM 里还有 GC、上下文切换、锁竞争和任务耗时抖动,数字不会这么整齐。
它适合用来理解「为什么会拒绝」和「为什么线程不扩」,不适合拿来定生产参数。
下一步我想把任务耗时改成随机分布,再加一条「提交方被 CallerRuns 阻塞」的独立时间线。
那样就能看到上游到底被拖慢了多少。
八、准备环境:进入码道 Web
浏览器打开码道 Web 版: https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwlb&sourcead=dmzntgwlb1&invitecode=INV-IXhxk5bwWx_qKSP1FcgSuiOEf6ruDXtjvLFiCH9nMs5dZGLapaSSylwt-cKRj5LoT3MlxikWxy8VhqplfvtQjefQOq31Tbp3R3wYP3K535Cn4XTaUzqqExKPYTWvvw4LijV14IS1torXdTGp7oyXb6SR5VxgYTg50jM ,登录后就能在对话窗口输入需求,不需要装软件。
码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。
我当时的描述很具体:单文件、虚拟时钟、四种拒绝策略、17 项断言、失败路径。
需求写得越像验收清单,生成出来的页面越接近能直接跑的状态。
页面生成后,我在本地跑了一遍,又补了 maximumPoolSize=0 的校验。

浙公网安备 33010602011771号