学完这篇你能得到:一张能直接照着配的 Java 线程池参数表,以及四种拒绝策略到底该选哪个的判断依据。
第一步:先认清 7 个构造参数
ThreadPoolExecutor 只有这一个核心构造器,7 个参数各管一摊:
new ThreadPoolExecutor(
4, // corePoolSize 核心线程数,常驻
8, // maximumPoolSize 最大线程数
60L, TimeUnit.SECONDS, // keepAliveTime+unit 非核心线程空闲存活时间
new ArrayBlockingQueue<>(200), // workQueue 任务排队队列
new ThreadFactory() { ... }, // threadFactory 线程怎么造(建议自定义命名)
new ThreadPoolExecutor.AbortPolicy() // handler 拒绝策略
);
注意:corePoolSize 的线程默认是「有任务才创建」,不是启动就建好。想让核心线程提前建好,调 prestartAllCoreThreads(),否则第一波请求会有建线程的延迟。
第二步:记住任务提交后的执行顺序
这是理解一切配置的前提,顺序是固定的四步:
- 当前线程数 < corePoolSize → 直接新建核心线程执行;
- 否则任务进
workQueue 排队;
- 队列满了且线程数 < maximumPoolSize → 新建非核心线程执行;
- 队列满了且线程数已达上限 → 交给拒绝策略。
注意:第 2 步在第 3 步之前。这意味着如果你用无界队列,maximumPoolSize 永远不会生效,等于白设。很多人配了 200 最大线程却从没生效过,原因就在这。
第三步:队列类型决定线程池的性格
ArrayBlockingQueue(n):有界,最推荐,容量自己说了算;
LinkedBlockingQueue():不传容量时默认 Integer.MAX_VALUE,近似无界,最大线程数和拒绝策略双双失效,任务堆积到 OOM;
SynchronousQueue:不存储,来任务必须立刻有线程接,适合短任务高并发(newCachedThreadPool 用它);
PriorityBlockingQueue / DelayQueue:带优先级、带延迟的场景专用。
注意:Executors.newFixedThreadPool() 和 newSingleThreadExecutor() 用的都是无界 LinkedBlockingQueue,newCachedThreadPool() 最大线程数是 Integer.MAX_VALUE。这三种都建议手写构造器替代,别图省事。
第四步:线程数按任务类型算
没有万能值,看任务是 CPU 密集还是 IO 密集:
- CPU 密集型(计算、加解密):
N + 1,N 为核数,多一个兜底防偶发阻塞;
- IO 密集型(数据库、HTTP 调用):
N × (1 + 等待时间/计算时间),经验值常用 2N 起;
- 拿不准就压测。先设
2N,观察 getActiveCount() 和队列堆积量,再往下调。
两个辅助指标要盯住:getQueue().size() 持续增长说明消费不过来,getActiveCount() 长期接近 maximum 说明该扩容或该加机器了。
第五步:拒绝策略选哪个
线程池满了总得有个说法,JDK 内置四种:
| 策略 | 行为 | 适用 |
|---|
AbortPolicy(默认) | 抛 RejectedExecutionException | 必须感知失败的场景,配合调用方 catch |
CallerRunsPolicy | 由提交任务的线程自己跑 | 能接受降速、不想丢任务,天然形成背压 |
DiscardPolicy | 静默丢弃,无任何提示 | 几乎不建议,日志埋点型任务除外 |
DiscardOldestPolicy | 丢掉队头最老的任务,再重试提交 | 只保留最新数据的场景(如行情刷新) |
注意:DiscardPolicy 和 DiscardOldestPolicy 都不抛异常、不打日志,任务凭空消失时你会完全无感。线上务必至少打一条告警日志。
第六步:自定义拒绝策略 + 优雅关闭
多数生产环境需要的是「兜底 + 可观测」,自己写一个:
RejectedExecutionHandler handler = (r, pool) -> {
log.warn("线程池已满, active={}, queue={}",
pool.getActiveCount(), pool.getQueue().size());
// 落库重试 / 降级返回 / 抛业务异常
throw new RejectedExecutionException("task rejected");
};
关闭时别直接 shutdownNow(),正确姿势是:
pool.shutdown(); // 不再接新任务,跑完队列里的
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
pool.shutdownNow(); // 超时强制中断
}
注意:shutdownNow() 会中断正在跑的任务并返回未执行的任务列表,直接丢列表等于丢数据。另外 awaitTermination 必须配合 shutdown() 先调用,否则会一直等。
小结
- 7 个参数里,队列容量和拒绝策略是线上事故的高发区,必须显式指定;
- 执行顺序是「核心线程 → 队列 → 扩容 → 拒绝」,先排队后扩容;
- 无界队列会让
maximumPoolSize 和拒绝策略同时失效;
- 线程数按 CPU 密集
N+1、IO 密集 2N 起步,靠监控调优;
- 拒绝策略优先考虑
CallerRunsPolicy 或自定义日志版,慎用静默丢弃;
- 关闭走
shutdown() + awaitTermination() + 超时兜底。