线程池设计的核心权衡:如何在“资源利用率”和“应对突发负载”之间取得平衡。
简单回答:核心线程数(corePoolSize)定义了线程池的“基线容量”,用于处理常规负载;最大线程数(maximumPoolSize)定义了线程池的“峰值容量”,用于应对短时的流量高峰。而非核心线程就是用来填充这个峰值容量的临时工,它们会在任务高峰时被创建,在空闲超时后被销毁,从而避免长期占用系统资源。
1. 为什么不能只设一个固定的线程数?
如果只设一个固定值(比如 10 个线程),会遇到两个极端问题:
| 方案 | 问题 |
|---|---|
| 固定值设得太小 | 高峰期任务堆积严重,响应时间变长,甚至队列爆满导致任务丢失。 |
| 固定值设得太大 | 平时大量线程闲置,白白消耗内存和 CPU(上下文切换开销),资源浪费。 |
所以需要动态伸缩的能力:平时保持少量核心线程,高峰时临时扩容,高峰过后自动缩容。
2. 核心线程 vs 非核心线程
| 维度 | 核心线程 | 非核心线程 |
|---|---|---|
| 创建时机 | 提交任务且当前线程数 < corePoolSize 时立即创建 | 队列已满且当前线程数 < maximumPoolSize 时创建 |
| 存活策略 | 默认一直存活(除非设置 allowCoreThreadTimeOut(true)) | 空闲超过 keepAliveTime 后被回收 |
| 角色定位 | 长期稳定的“正式员工” | 应对突发的“临时工” |
设计意图:
- 核心线程:处理常规流量,避免频繁创建销毁线程的开销。
- 非核心线程:在流量尖峰时临时增加处理能力,防止任务积压;尖峰过去后自动退出,释放资源。
3. 非核心线程的具体作用场景
假设配置:corePoolSize=5,maximumPoolSize=20,workQueue 是一个容量为 10 的 ArrayBlockingQueue。
- 正常情况:每秒提交 3 个任务,5 个核心线程足够处理,队列始终为空,不会创建非核心线程。
- 突发情况:某时刻突然提交 30 个任务。
- 5 个核心线程立刻开始处理 5 个任务。
- 剩余 25 个任务尝试入队,队列最多容纳 10 个,所以 10 个入队成功。
- 还有 15 个任务无法入队,线程池发现当前线程数(5)< maximumPoolSize(20),于是创建非核心线程来处理这些任务,最多创建到 20 个线程。
- 最终 20 个线程并发处理,队列中还有 10 个在排队。这 20 个线程会尽快消化积压任务。
- 尖峰过后:当队列被清空,并且所有线程都空闲超过
keepAliveTime(比如 60 秒)时,非核心线程(15 个)会被回收,线程池恢复为 5 个核心线程。
如果没有非核心线程:最大线程数 = 核心线程数 = 5,那么队列满后剩余任务会被直接拒绝(触发拒绝策略),导致业务失败。
4. 为什么不让核心线程也超时回收?
可以,通过 allowCoreThreadTimeOut(true) 可以让核心线程也超时退出。但通常不这么做,因为:
- 创建线程是有开销的(分配栈内存、初始化等),对于长期运行的服务,保留少量核心线程可以快速响应常规请求。
- 核心线程数一般设置得比较合理(如 CPU 核心数 + 1),这些线程即使空闲,消耗的资源也很有限。
allowCoreThreadTimeOut 通常用于非常间歇性的任务场景(比如每天只跑几次批处理),希望线程池在空闲时能完全释放所有线程。
5. 最大线程数是不是越大越好?
不是。maximumPoolSize 受限于系统资源(CPU、内存、文件句柄、网络连接等)。如果设置过大:
- 线程上下文切换开销增加,反而降低吞吐量。
- 每个线程默认占用约 1 MB 栈内存(可调),太多线程会导致内存溢出。
- 大量并发线程可能压垮数据库、下游服务。
所以 maximumPoolSize 应该根据系统承载能力上限和业务允许的最大并发来设定,而不是随意给个很大的数。
6. 总结一句话
核心线程是“常规部队”,非核心线程是“临时增援”。有了最大线程数,线程池就能在平时节约资源,在高峰时自动扩容,实现弹性伸缩,这是高性能并发系统设计的常见模式。