AIGC标识 ThreadPool 7 个参数怎么配?100 个任务压进去看 4 种拒绝策略

corePoolSize=4、maximumPoolSize=8、queueCapacity=10,100 个任务每 20ms 提交一个,每个跑 250ms。

AbortPolicy 完成 70 个,拒绝 30 个。CallerRunsPolicy 完成 100 个,总耗时从 2520ms 涨到 3310ms。

我把这条派发链路做成了一个能拖时间轴的沙盘。参数一改,worker 泳道、等待队列和拒绝清单会同步变化。

图 1:默认参数跑完 100 个任务,8 条 worker 泳道和 30 个被拒绝任务都在画面上

一、先说结论:队列决定线程扩不扩

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);

这段代码不是我事后补的说明。页面左下角就贴着同一份逻辑。

图 2:跑到一半时队列 10/10 已满,8 个 worker 都在忙,拒绝清单开始出现任务

四、数据: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。

它优化的是排队时间,不是完成率。

图 3:四种策略的完成、拒绝、丢弃构成和总耗时对比

五、验证与踩坑: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」。

图 4:maximumPoolSize 填 0 时,参数面板变红、运行按钮禁用,并给出中文错误提示

六、本地复现:一个 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 的校验。

图 5:页面底部的派发逻辑和当前参数生成的等价 Java 构造参数

posted @ 2026-11-07 06:40  新科技观察员  阅读(2)  评论(0)    收藏  举报