适用读者:配过线程池但没算过"到底能接多少任务"的人。
前置:S05-B02。
实测环境:OpenJDK 21.0.12.1(2026-11-05)。
覆盖旧文:A-1127(483) A-1126(385) A-2635(273) + 919(96) 920(77) 923(128) 922(91) 1555(128) 1552(118) 1599(200) 1581(116) 387(145) 2663(89) = 2,329 点。
覆盖扩展:E-S05-007(执行流程与拒绝策略)。
上一篇:S05-B04 · ThreadLocal:结构、场景与泄漏
下一篇:S05-J01 · AQS 与 ReentrantLock。
1. 一句话结论
线程池的容量不是"最大线程数",而是"核心线程 + 队列容量"。 超过就按拒绝策略处理——很多线上"任务丢失"就是没算这一条。
2. 实测:核心 2 + 队列 2 = 只能接 4 个
ThreadPoolExecutor ex = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.AbortPolicy());
实测输出:
提交任务 1:接受(queue=0)
提交任务 2:接受(queue=0)
提交任务 3:接受(queue=1)
提交任务 4:接受(queue=2)
提交任务 5:**被拒绝**(AbortPolicy)
| 阶段 | 行为 |
|---|---|
| 任务 1–2 | 创建核心线程直接跑 |
| 任务 3–4 | 进队列(容量 2) |
| 任务 5 | 队列满、线程已达 max → 拒绝 |
注意:这里的 maximumPoolSize = 2 与核心相同,所以"第 3 个任务不会创建新线程"。如果 max > core,队列满后会先扩到 max,再拒绝。
3. 执行流程(E-S05-007)
submit(task)
├─ 核心线程未满 → 新建核心线程执行
├─ 核心满 → 入队列
│ ├─ 入队成功 → 等空闲线程取
│ └─ 队列满 → 尝试扩到 max
│ ├─ 未到 max → 新建非核心线程
│ └─ 已到 max → 拒绝策略
四种拒绝策略:
| 策略 | 行为 | 适用 |
|---|---|---|
AbortPolicy(默认) |
抛 RejectedExecutionException |
必须感知失败 |
CallerRunsPolicy |
调用方线程自己跑 | 天然背压(会拖慢提交方) |
DiscardPolicy |
静默丢弃 | 危险(任务无声消失) |
DiscardOldestPolicy |
丢队列里最老的 | 只要最新数据 |
判据:"任务丢了会怎样?" 不能丢 → AbortPolicy + 上层重试/告警;可丢 → Discard*;要背压 → CallerRunsPolicy。
4. 为什么阿里禁用 Executors(A-1581)
| 工厂方法 | 隐患 |
|---|---|
newFixedThreadPool |
队列是无界 LinkedBlockingQueue → 任务堆积导致 OOM |
newCachedThreadPool |
max = Integer.MAX_VALUE → 线程爆炸 |
newSingleThreadExecutor |
无界队列(同上) |
newScheduledThreadPool |
无界延迟队列 |
结论:手动 new ThreadPoolExecutor,显式指定队列容量与拒绝策略。
5. 参数怎么定(可算的一条)
线程数(CPU 密集)≈ 核数 + 1
线程数(IO 密集) ≈ 核数 × (1 + 等待时间/计算时间)
队列容量 ≈ 峰值 QPS × 可接受等待时间
| 参数 | 建议 |
|---|---|
corePoolSize |
CPU 密集 = 核数+1;IO 密集按上式 |
maximumPoolSize |
通常 = core(避免抖动)或略大 |
keepAliveTime |
非核心线程空闲回收时间 |
workQueue |
必须有界(ArrayBlockingQueue(n)) |
threadFactory |
自定义线程名(便于 jstack 定位) |
handler |
显式指定拒绝策略 |
allowCoreThreadTimeOut |
低峰期回收核心线程 |
6. 五个误区
| 误区 | 真相 |
|---|---|
| "线程池能接无限任务" | 核心 + 队列上限(实测 4 个) |
"newFixedThreadPool 安全" |
无界队列会 OOM |
| "拒绝策略默认会重试" | 默认直接抛异常 |
"DiscardPolicy 只是少跑一点" |
任务静默消失,可能丢关键业务 |
| "线程越多吞吐越高" | 受核数与切换成本限制 |
7. 自检
- 实测里为什么第 5 个任务被拒?如果 max > core 会怎样?
- 四种拒绝策略各自的适用场景?
- 为什么阿里禁用
Executors?举两个具体隐患。 - IO 密集型线程数怎么估算?
- 队列为什么必须有界?
8. 下一篇
S05-J01 · AQS 与 ReentrantLock 源码
线程池讲完,进阶层开始:ReentrantLock 背后那个"排队等锁"的结构——AQS 的 state 与 CLH 队列。
本篇属于系列《S05 · Java 并发与 JVM》。旧文来源:A-1127/1126/2635/919/920/923/922/1555/1552/1599/1581/387/2663;扩展点 E-S05-007。拒绝策略实测为 OpenJDK 21。