Java
ThreadPoolExecutor:从任务提交到拒绝策略
线程池的价值不只是“线程复用”。它更重要的作用是为系统建立资源边界:限制线程数量、控制任务排队,并在过载时给出明确策略。
任务提交后的顺序
ThreadPoolExecutor 接收任务后,按下面的顺序处理:
核心线程未满 → 创建核心线程
核心线程已满 → 尝试进入任务队列
队列已满 → 创建非核心线程,直到最大线程数
仍无法承载 → 执行拒绝策略
一个常见误区是认为任务会先把线程数扩展到最大值。实际上,核心线程满后通常先排队,只有队列也满了才继续创建线程。
参数背后的问题
corePoolSize 和 maximumPoolSize 控制并发上限,workQueue 决定可缓冲多少任务,keepAliveTime 控制空闲非核心线程的回收,ThreadFactory 负责创建可识别的线程,RejectedExecutionHandler 定义过载行为。
如果使用无界队列,maximumPoolSize 往往很难生效,任务还可能持续堆积直到内存溢出。因此企业项目通常显式创建 ThreadPoolExecutor,而不是直接采用不透明的 Executors 默认配置。
四种拒绝思路
- AbortPolicy:直接抛异常,让上层感知失败。
- CallerRunsPolicy:由提交者执行,形成自然的反压。
- DiscardPolicy:静默丢弃,需要确认业务允许。
- DiscardOldestPolicy:丢弃最旧任务后重试。
不存在通用的“最佳策略”。支付、日志、缓存刷新对任务丢失的容忍度完全不同。
怎样估算线程数
CPU 密集任务的线程数通常接近 CPU 核心数;IO 密集任务因为大量时间用于等待,可以适当增加。但公式只能作为起点,最终应依据任务耗时、队列长度、P95/P99 延迟、错误率和机器资源,通过压测调整。
在线程池问题上,我更关注三个信号:任务提交速度是否长期高于消费速度、队列满时业务如何退化、异常能否被监控。参数只是表面,系统在过载时是否可控才是核心。