把 ThreadPoolExecutor 讲透:6 个核心参数 + 4 种拒绝策略(附完整示例)
线程池的本质是两件事:限制并发上限 + 复用线程 。ThreadPoolExecutor 把这两件事"参数化"到了极致:线程什么时候创建、最多创建多少、任务往哪儿放、放不下怎么办、线程长什么样------全都能配。
- 提交任务时到底发生了什么(核心调度逻辑)
- 6 个关键参数逐个解释(怎么影响行为)
- 4 种拒绝策略逐个解释(队列满 + 线程也满时怎么办)
1. 先搞清楚:提交一个任务时,线程池按什么顺序"消化"它?
可以把 execute/submit 的处理流程记成一条很硬的规则链:
-
当前线程数 <
corePoolSize→ 直接创建新线程来跑这个任务(即使队列里还有空位也优先扩到核心线程数)
-
当前线程数 ≥
corePoolSize→ 尝试把任务放进
workQueue -
队列满了(放不进去)
- 如果当前线程数 <
maximumPoolSize
→ 继续创建新线程(非核心线程)来跑这个任务 - 否则(线程也到上限了)
→ 触发拒绝策略RejectedExecutionHandler
- 如果当前线程数 <
这一条链路把"什么时候扩线程、什么时候排队、什么时候拒绝"彻底定死了。
2. 6 个核心参数:每个参数到底控制什么?
2.1 corePoolSize:核心线程数("长期雇佣的正式员工")
- 线程池希望长期维持的线程数量
- 当任务来时,如果线程数不足核心数,倾向于立刻创建线程执行任务
- 核心线程默认不回收 (除非显式开启
allowCoreThreadTimeOut(true))
直觉:corePoolSize 决定"常态吞吐的底盘"。
2.2 maximumPoolSize:最大线程数("极限扩容上限")
- 队列满了以后,线程池还可以继续扩线程,但最多只能扩到
maximumPoolSize - 达到最大线程数后,再来任务且队列也满 → 只能拒绝
直觉:maximumPoolSize 决定"洪峰时最多能拉多少临时工"。
重要现实:如果
workQueue是无界队列 (例如默认LinkedBlockingQueue不给容量),队列几乎不会满,线程池通常就很难扩到 maximum,表现会更像"固定线程池"。
2.3 keepAliveTime:非核心线程的空闲回收时间("临时工多久没活就解雇")
- 主要作用于 超过核心数的那部分线程(非核心线程)
- 当这些线程空闲超过
keepAliveTime,就会被回收,线程数逐步回落到corePoolSize - 配合
TimeUnit使用,比如 60 秒
直觉:keepAliveTime 决定"洪峰过去后回收速度"。
补充:开启
allowCoreThreadTimeOut(true)后,核心线程也会按这个时间回收,适合极度节省资源的场景。
2.4 workQueue:任务队列("排队等候区")
任务先排队还是先扩线程,很大程度取决于队列类型。常见队列选择会直接改变线程池性格:
① ArrayBlockingQueue(有界数组队列)
- 有界,容量固定
- 队列满了就会促使线程池扩线程,直到
maximumPoolSize - 更容易形成"排队有限 + 扩容明显 + 可控拒绝"的形态
适合:希望明确限制内存占用、希望洪峰时扩线程但别无限堆任务。
② LinkedBlockingQueue(链表队列)
- 可以有界,也可以无界(不传容量时近似无界)
- 无界时:任务会疯狂堆积,线程数往往停在
corePoolSize附近,maximumPoolSize基本用不上 - 风险:积压太多任务导致内存压力,甚至 OOM
适合:任务必须排队、洪峰也不想扩线程太猛、且能接受排队延迟的场景(通常建议给容量)。
③ SynchronousQueue(不存任务,直接"交接")
- 容量为 0,没有真正的队列
- 来一个任务就必须找到一个空闲线程接手;接不到就扩线程(直到
maximumPoolSize),再接不到就拒绝 - 这会让线程池变得非常"激进扩容"
适合:任务很短、希望低延迟、不希望排队(更像 newCachedThreadPool 的核心组件)。
2.5 threadFactory:线程工厂("线程长什么样")
线程池创建新线程时会调用 threadFactory.newThread(runnable)。常用用途:
- 给线程统一命名(排障神器)
- 设置是否守护线程(daemon)
- 设置优先级
- 设置未捕获异常处理器(
UncaughtExceptionHandler)
默认线程名往往是 pool-1-thread-1 这种形式;生产环境强烈建议自定义命名,日志和 dump 会友好很多。
2.6 RejectedExecutionHandler:拒绝策略("队列满 + 线程到上限时的最后裁决")
当同时满足:
workQueue放不下任务(满了)- 当前线程数已经达到
maximumPoolSize
此时线程池没法再接任务,只能交给拒绝策略处理。
3. 四种拒绝策略:到底"拒绝"是什么意思?
JDK 内置 4 个实现(名字就很直白):
3.1 AbortPolicy:直接抛异常(默认)
- 直接抛
RejectedExecutionException - 最"硬",能让调用方立刻意识到系统过载
适合:任务不能悄悄丢、必须显式失败并被上层处理的场景。
3.2 CallerRunsPolicy:调用者执行(用提交线程"顶上去")
- 不新建线程、不入队
- 直接让提交任务的线程自己运行
task.run()
效果:提交方被"拖慢",形成一种天然的反压(backpressure),让系统自己降速。
适合:允许提交方变慢、希望通过反压保护系统的场景(例如网关层、消费端限流)。
3.3 DiscardOldestPolicy:丢最老(丢队列头部)再尝试提交
- 丢弃队列中"等待最久"的那个任务(队首)
- 然后再尝试把新任务放进去(通常会成功)
适合:更关心"最新任务",旧任务过期就没意义的场景(例如状态刷新、UI 更新、某些监控采样)。
风险:被丢的任务不会执行,必须确认业务允许。
3.4 DiscardPolicy:丢最新(直接丢当前任务)
- 当前提交的任务直接被丢弃
- 不抛异常
适合:允许少量任务悄悄丢失、且不希望影响主流程的场景(例如非关键日志、非关键统计上报)。
风险:最"静默",排障时容易误以为任务执行了。
4. 一份完整示例:把 6 个参数 + 拒绝策略串起来
来看这段最常用的配置模板:有界队列 + 明确最大线程 + 命名线程 + 自选拒绝策略。
java
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolExecutorDemo {
public static void main(String[] args) {
int corePoolSize = 4;
int maximumPoolSize = 8;
long keepAliveTime = 60L;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger idx = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("biz-worker-" + idx.getAndIncrement());
t.setDaemon(false);
return t;
}
};
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
// 可替换:
// new ThreadPoolExecutor.AbortPolicy()
// new ThreadPoolExecutor.DiscardOldestPolicy()
// new ThreadPoolExecutor.DiscardPolicy()
ThreadPoolExecutor pool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime, TimeUnit.SECONDS,
workQueue,
threadFactory,
handler
);
for (int i = 0; i < 1000; i++) {
final int id = i;
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " running task " + id);
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
});
}
pool.shutdown();
}
}
理解这段配置时,抓住三件事就够了:
- 常态并发 :由
corePoolSize决定 - 洪峰策略 :队列满后是否扩线程,由
workQueue+maximumPoolSize决定 - 过载策略 :扩也扩不动时怎么办,由
RejectedExecutionHandler决定
5. 选型与调参的"工程化口诀"
- 想要"可控" → 优先有界队列(别让任务无限堆)
- 想要"低延迟不排队" →
SynchronousQueue(但要小心线程暴涨) - 允许"降速保护系统" →
CallerRunsPolicy是最实用的反压手段 - 任务绝不能丢 → 别用
Discard*,用AbortPolicy并在上层兜底 - 排障要轻松 → 自定义
threadFactory命名线程(dump 一眼看穿)
把 ThreadPoolExecutor 看成一个"可编程的调度器"会更顺手:
corePoolSize/maximumPoolSize/keepAliveTime 决定"人手规模与回收";workQueue 决定"排队策略";RejectedExecutionHandler 决定"过载时的业务选择"。