📌 线程池配置回顾:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize 核心线程数
4, // maximumPoolSize 最大线程数
60, TimeUnit.SECONDS, // 非核心线程空闲存活时间
new ArrayBlockingQueue<>(2),// 有界任务队列(最多容纳2个任务)
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
你随后提交了 10 个任务:
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " is executing");
});
}
🚀 执行过程详解(任务 i = 0 到 9)
提交顺序 | 当前情况 | 执行结果 |
---|---|---|
i = 0 | 核心线程数 < 2 → 创建线程执行任务 | 创建线程1执行 |
i = 1 | 核心线程数 < 2 → 创建线程执行任务 | 创建线程2执行 |
i = 2 | 核心线程已满,队列未满 → 入队 | 放入队列 |
i = 3 | 核心线程已满,队列未满 → 入队 | 放入队列 |
i = 4 | 核心线程满 + 队列满 → 创建非核心线程 | 创建线程3执行 |
i = 5 | 核心线程满 + 队列满 → 创建非核心线程 | 创建线程4执行 |
i = 6 | 核心 + 非核心线程都满,队列也满 | 启动拒绝策略 → 调用者线程执行 |
i = 7 | 同上 → 调用者线程执行任务 | 主线程执行 |
i = 8 | 同上 → 调用者线程执行任务 | 主线程执行 |
i = 9 | 同上 → 调用者线程执行任务 | 主线程执行 |
🧠 可视化总结(线程池内部状态)
阶段 | 核心线程 | 队列容量 | 非核心线程 | 调用者线程(main) |
---|---|---|---|---|
初始 | 0 | 2 | 0 | 提交任务 |
第1~2任务 | 2 | 2 | 0 | 提交任务 |
第3~4任务 | 2 | 2(满) | 0 | 提交任务 |
第5~6任务 | 2 | 满 | 2(满) | 提交任务 |
第7~10任务 | 2 | 满 | 满 | 自己执行任务 |
✅ 重点理解:
-
核心线程(2个)先满
-
有界队列(2个)接收后两个任务
-
超过核心 + 队列容量后,线程池最多扩容到 4 个线程
-
超出最大线程数且队列满时 → 启动拒绝策略
-
CallerRunsPolicy
被触发,主线程(main
)阻塞执行任务-
注意:这会导致
for
循环的每次提交都会被主线程“卡住”,从而减慢提交速度,形成“背压”
-
❗ 现实意义:
-
在实际业务中,例如 Web 应用,若主线程执行任务过久,会导致响应超时
-
所以要搭配超时控制、监控等保护措施
问题一:第二个任务时,如果第一个线程已完成,线程池是复用它还是新建核心线程?
✅ 答案:不会新建核心线程,会复用已经空闲的核心线程
-
核心线程池是可复用的,线程池始终尝试复用现有空闲线程来执行新任务;
-
哪怕核心线程数量未达到
corePoolSize
,也不会无脑创建新线程; -
只有在所有可用线程都忙,且核心线程数量没达到上限,才会新建核心线程。
🧪 举个实际例子:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.CallerRunsPolicy()
);
你依次提交两个任务,任务很短(几毫秒就完成):
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " Task1");
});
Thread.sleep(50); // 等待一下,让任务1执行完
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " Task2");
});
执行过程:
-
第一个任务启动时,线程池为空 → 创建核心线程 T1 执行 Task1
-
Task1 很快执行完,T1 变为空闲
-
第二个任务提交时,线程池会复用 T1 来执行 Task2,不会新建 T2
✅ 结论 1:线程池优先复用线程,不会因核心线程未满而强制新建线程
问题二:核心线程数(corePoolSize)和最大线程数(maximumPoolSize)有什么区别?
项目 | 核心线程数 corePoolSize | 最大线程数 maximumPoolSize |
---|---|---|
定义 | 线程池启动后,始终维持的最小线程数量 | 线程池在高并发时可扩展到的最大线程数 |
特点 | 永不销毁(除非 allowCoreThreadTimeOut ) | 非核心线程超时后会被回收 |
创建时机 | 一有任务提交就会创建 | 只有当核心线程 + 队列都满后,才会创建 |
生命周期 | 默认情况下永久存在 | 空闲超过 keepAliveTime 后自动销毁 |
适用场景 | 保证服务常驻线程 | 高峰时短暂扩容使用 |
📌 举例说明:
corePoolSize = 2
maximumPoolSize = 5
-
正常情况下,最多只保留 2 个核心线程运行
-
当任务量大到撑爆任务队列后,线程池会逐步向最大线程数扩容(最多 5 个)
-
任务处理完成后,多出的线程(3 个)在空闲一段时间后被销毁
✅ 结论 2:
corePoolSize
是线程池“基础承载能力”,maximumPoolSize
是“应急爆发力”。
你可以这样记:
“核心是常驻兵,最大是应急兵。”
问题三:有界队列 vs 无界队列
项目 | 有界队列 (ArrayBlockingQueue ) | 无界队列 (LinkedBlockingQueue ) |
---|---|---|
是否有容量限制 | ✅ 是 | ❌ 否(默认 Integer.MAX_VALUE ) |
当队列满后行为 | 启动拒绝策略 or 扩展线程数 | 不会满,不会触发拒绝策略 |
是否易 OOM | ✅ 安全 | ❌ 高并发下极易内存撑爆 |
对 maximumPoolSize 的影响 | 会生效:线程数会扩展 | 几乎无效,核心线程之外线程不建 |
是否适合生产使用 | ✅ 推荐(配合合理容量) | ⚠️ 小心使用,仅限控制流场景 |
使用场景举例 | 日志落盘、批量数据处理等 | 少量任务、非常轻量任务场景 |
Java 线程池中它们怎么影响行为?
我们看下线程池提交任务时的内部逻辑核心段:
if (workerCount < corePoolSize) {
// 创建核心线程
} else if (queue.offer(task)) {
// 队列没满:任务排队
} else if (workerCount < maximumPoolSize) {
// 扩展线程数
} else {
// 启动拒绝策略
}
✔ 有界队列(如 ArrayBlockingQueue
)
-
核心线程数满后,新任务先进入队列;
-
队列满了,才会考虑创建新线程(到 maxPoolSize);
-
线程 + 队列都满了,就触发拒绝策略;
-
安全,但吞吐量受限。
❌ 无界队列(如 LinkedBlockingQueue
)
-
核心线程满后,任务总是进入队列;
-
队列永远不会满,线程数永远不会增加到 maxPoolSize;
-
maximumPoolSize 形同虚设;
-
风险:如果任务提交速度 > 处理速度,队列无限增长,最终 OOM。
🚨 真实案例:无界队列导致 OOM
很多开发者一开始使用:
Executors.newFixedThreadPool(n) // 内部是无界队列
然后遇到:
-
系统运行一段时间后直接崩溃;
-
OutOfMemoryError: Java heap space
; -
用 heap dump 工具发现:大量 Runnable 排在队列中无法被处理。
✅ 正确使用建议
场景 | 建议用什么队列 |
---|---|
日志异步落盘(可丢、低延迟) | 有界队列 + DiscardPolicy |
数据分析 / 批量处理任务 | 有界队列 + CallerRuns |
核心 Web 请求处理 | 有界队列 + 监控拒绝策略 |
轻量级任务,且处理速度快 | 可考虑无界队列,但要限流 |
不知道任务量的动态任务流 | MQ + 有界线程池 |
📌 总结一句话
无界队列虽然简单,但极易造成内存溢出;有界队列虽然吞吐有限,但可控、稳定,是生产项目的首选。