线程池七大参数怎么设置合理
更新时间:2026-09-18。线程池用少量线程复用处理大量任务,避免反复创建销毁。配错参数会导致 OOM、雪崩或吞吐崩塌。相关:ThreadLocal 线程池串号、服务熔断隔离、jstack 看线程状态。
一、七大参数
new ThreadPoolExecutor(
int corePoolSize, // 1 核心线程数(即使空闲一般也保留)
int maximumPoolSize, // 2 最大线程数
long keepAliveTime, // 3 非核心线程空闲回收时间
TimeUnit unit, // 4 时间单位
BlockingQueue<Runnable> q, // 5 工作队列
ThreadFactory factory, // 6 线程工厂(命名、守护、优先级)
RejectedExecutionHandler h // 7 拒绝策略
);| 参数 | 作用 | 设置要点 |
|---|---|---|
| corePoolSize | 常驻线程数 | 按任务类型与目标并行度定 |
| maximumPoolSize | 上限 | core 与 max 之间为「应急非核心线程」 |
| keepAliveTime/unit | 非核心空闲存活 | 回收突发线程;allowCoreThreadTimeOut(true) 也回收核心 |
| workQueue | 待执行任务队列 | 必须有界,防无限堆积 OOM |
| threadFactory | 创建线程 | 给线程起业务名,jstack/Arthas 才看得懂 |
| handler | 满了怎么办 | 按业务选拒绝/降级,别静默丢弃 |
二、提交流程(很多人理解错了顺序)
新任务进来:
1) 运行线程 < core → 新建核心线程执行(哪怕有空闲核心线程也要到 core 才复用,
实际是:少于 core 直接建新线程)
2) ≥ core 且队列没满 → 入队等待
3) 队列满 且 线程 < max → 新建「非核心」应急线程执行
4) 队列满 且 线程 = max → 执行拒绝策略关键:队列优先级高于非核心线程。 用无界/很大的队列时,队列永远填不满,maxPoolSize 永远不会生效,突发流量全堆在队列里(延迟暴涨甚至 OOM),而不是触发扩容或拒绝。
三、为什么不要用 Executors 的快捷工厂
newFixedThreadPool/newSingleThreadExecutor:用的是无界 LinkedBlockingQueue,任务积压无上限 → OOM。newCachedThreadPool:maximumPoolSize=Integer.MAX_VALUE,可创建海量线程 → 线程数爆炸(每线程默认栈 ~1MB,内存耗尽、上下文切换风暴)。 生产应显式new ThreadPoolExecutor并用有界队列。
四、线程数怎么估
| 任务类型 | 经验公式 | 直觉 |
|---|---|---|
| CPU 密集(计算、序列化) | N + 1(N=可用核数) | 线程太多只会徒增切换,多出 1 个应对偶发缺页/GC |
| IO 密集(RPC/DB/HTTP 等待) | N × (1 + 等待时间/计算时间),常取 2N~10N | 等待时线程阻塞不占 CPU,可多开覆盖等待 |
Little 定律也常用:并发线程数 ≈ 目标 QPS × 单任务平均耗时(s)。例如每请求平均 50ms、要扛 1000 QPS,需要约 50 个在途处理位(再考虑串行内多次下游调用)。
公式只是起点:必须以压测(wrk)+ 监控(线程池活跃数、队列深度、拒绝数、RT p99)校准。数据库等下游连接池也是上限,线程数远超连接数只会排队等连接。
五、队列与拒绝策略
| 拒绝策略 | 行为 | 适用 |
|---|---|---|
| AbortPolicy(默认) | 抛 RejectedExecutionException | 让调用方感知,需捕获处理 |
| CallerRunsPolicy | 谁提交谁自己跑 | 天然反压:拖慢生产者,不丢任务,削峰 |
| DiscardPolicy / DiscardOldestPolicy | 静默丢弃/丢最老 | 可丢弃任务(日志、采样),慎用 |
| 自定义 | 落 MQ/写盘/返回降级/告警 | 生产推荐,保证可观测、可重试 |
六、工程实践
- 线程池命名 + 多池隔离:不同业务(订单、报表、外部调用)独立线程池,互相不拖垮,配合舱壁隔离。
- 有界队列:按可接受排队时长 × 到达速率估容量。
- 可监控:暴露 activeCount、queue.size、completedTaskCount、reject 计数。
- 动态调参:把 core/max 放配置中心,高峰前调大、低峰回收(ThreadPoolExecutor 支持 setter 即时生效)。
- 优雅关闭:
shutdown()+awaitTerminement,再shutdownNow(),配合请求摘流,避免任务硬丢。 - ThreadLocal 清理:池化线程必须在任务边界 remove,见串号坑。
七、常见坑
- 无界队列:max 失效、积压 OOM,是最常见配置事故。
- 线程数照抄 2N 不看任务:CPU 密集开太多反而切换开销,见上下文切换成本。
- 所有业务共用一个池:慢任务占满,核心业务被连累(缺少隔离)。
- 默认 Abort 不 catch:异常吞掉或打到顶层导致请求失败无降级。
- 线程不命名:出问题时 jstack 全是 pool-x-thread-y 无法对应业务。
- 只设线程池不设下游连接池:线程拿到任务却等不到 DB 连接,吞吐由更底层的池决定。
总结
ThreadPoolExecutor 的七大参数是 corePoolSize、maximumPoolSize、keepAliveTime/unit、有界 workQueue、threadFactory 和拒绝策略,提交顺序是“少于核心数先建核心线程,满了入队,队列满才建到最大线程数,再满才触发拒绝策略”,因此用无界队列会让最大线程数形同虚设并在突发时积压 OOM,这也正是不能直接用 Executors.newFixedThreadPool(无界队列)和 newCachedThreadPool(线程数无上限)的原因;线程数没有万能值,CPU 密集约 N+1、IO 密集约 N×(1+等待/计算) 或用 Little 定律 QPS×单任务耗时 估算,再以压测和队列深度/拒绝数/p99 校准且不能超过下游连接池容量;队列必须有界,拒绝策略按业务在 CallerRuns(天然反压)、自定义落 MQ 降级与谨慎丢弃之间选择,并做多池隔离、线程命名、监控暴露与动态调参、优雅关闭,同时在任务边界清理 ThreadLocal 防止复用串号。