Appearance
线程池参数怎么配:corePoolSize、maximumPoolSize、queue 和拒绝策略该怎么理解
并发问题里,线程池几乎绕不过去。真正麻烦的地方不是会不会用,而是参数一旦没配对,系统行为很容易和预期完全不一样。
先说结论
如果你只记三点,先记这三点:
- 不要默认无脑使用
Executors提供的现成线程池 - 线程池效果不是只看线程数,而是“核心线程数 + 队列 + 最大线程数 + 拒绝策略”一起决定
- 线程池参数必须结合任务类型来看,CPU 密集和 IO 密集的配置思路不一样
一、线程池的任务提交流程
理解参数之前,先把执行顺序想清楚。对于 ThreadPoolExecutor,提交一个任务后,大致是这样处理的:
- 如果当前运行线程数小于
corePoolSize,优先创建核心线程 - 如果核心线程够了,任务先尝试进入阻塞队列
- 如果队列满了,再看能不能继续创建线程,直到
maximumPoolSize - 如果线程数已经到上限且队列也满了,就执行拒绝策略
这意味着一个非常关键的事实:
队列类型不同,maximumPoolSize 的意义会很不一样。
二、四个最核心参数分别控制什么
1. corePoolSize
核心线程数,表示线程池愿意长期保留多少个线程。
它更像“基础产能”。
2. maximumPoolSize
线程池允许扩张到的最大线程数。
它更像“高峰兜底上限”。
3. workQueue
任务排队的地方。
常见选择有:
LinkedBlockingQueueArrayBlockingQueueSynchronousQueue
4. RejectedExecutionHandler
当线程池真的处理不过来时,最后怎么兜底。
常见策略:
AbortPolicy:直接抛异常CallerRunsPolicy:让提交任务的线程自己执行DiscardPolicy:直接丢弃DiscardOldestPolicy:丢掉最旧的任务
三、为什么不推荐直接用 Executors
很多人刚学线程池时会先接触:
Executors.newFixedThreadPool()Executors.newCachedThreadPool()Executors.newSingleThreadExecutor()
它们用起来很方便,但工程里更稳妥的做法通常是直接显式创建 ThreadPoolExecutor。
原因主要有两个:
- 某些默认队列可能过大,容易把压力堆到内存上
- 某些默认线程扩张策略太激进,容易在高峰期把机器拖垮
特别是:
- 无界队列容易导致任务大量堆积
- 线程数无限扩张容易导致上下文切换和资源竞争恶化
四、队列选型比很多人想象中更重要
LinkedBlockingQueue
特点:
- 容量可以很大,甚至默认接近无界
- 更适合任务堆积可接受、但不希望线程数快速膨胀的场景
风险:
- 流量顶上来时,问题可能不是立刻报错,而是请求越积越多
- 最终表现成延迟上升、堆内存上涨、系统整体变慢
ArrayBlockingQueue
特点:
- 有明确容量
- 更适合希望把压力边界控制清楚的场景
优点:
- 更容易做容量治理
- 系统满载时行为更可预期
SynchronousQueue
特点:
- 不真正存储任务
- 提交一个任务,必须马上交给线程处理
这意味着:
- 如果没有空闲线程,就会继续创建线程,直到
maximumPoolSize - 它适合吞吐优先、短任务较多的场景,但对参数控制要求更高
五、CPU 密集和 IO 密集,配置思路为什么不同
CPU 密集型任务
例如:
- 复杂计算
- 加解密
- 本地批量转换
这类任务主要耗 CPU,线程太多反而会增加切换成本。
常见思路:
- 核心线程数接近 CPU 核数
- 队列不要太大
- 拒绝策略要明确
IO 密集型任务
例如:
- 调远程接口
- 查数据库
- 读写文件
这类任务大量时间在等待外部资源,所以线程数通常可以比 CPU 核数更高一些。
但也不能无脑加大,因为最终仍会受数据库连接数、下游服务容量和机器内存限制。
六、一个更稳妥的配置模板
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8,
16,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);这段配置的思路是:
- 有一个稳定的基础并发能力
- 高峰期允许适度扩容
- 队列容量可控
- 压力过大时通过
CallerRunsPolicy反向限制调用方速度
实际数值不一定照抄,但这种“可控扩容 + 有界队列 + 明确兜底”的思路通常比默认配置更稳。
七、线程池最常见的几个误区
1. 只看线程数,不看队列
线程池处理能力不是由 maximumPoolSize 单独决定的。
如果队列是无界的,很多时候任务根本不会触发扩容。
2. 觉得队列越大越安全
队列大只是把问题延后,不一定是真正解决。
如果下游持续处理不过来,大队列只会把延迟和内存风险放大。
3. 拒绝策略随便选
拒绝策略其实是系统过载时的最后一道行为定义。
它直接决定:
- 是快速失败
- 是降速自保
- 还是悄悄丢任务
4. 一个线程池跑所有任务
不同任务的执行时间、阻塞特性和优先级差异很大,混在一起容易相互拖垮。
更稳妥的做法通常是按业务类型拆分线程池。
一句话总结
线程池参数不是孤立配置项,而是一组容量治理规则。
真正要配的不是“多少线程”,而是系统在高峰期准备怎样排队、怎样扩容、怎样拒绝,以及出问题时怎样可控地退化。