1. 引言
线程池是 Java 并发编程中绕不开的话题,也是面试高频考点。然而在实际生产环境中,线程池「爆掉」的问题屡见不鲜:任务堆积、拒绝异常、线程数失控、CPU 飙升……这些问题往往源于对线程池核心参数的理解不够深入,或者配置时凭感觉拍脑袋。
本文将从线程池的核心参数讲起,逐步深入到队列与拒绝策略的选择,再给出 CPU 密集与 IO 密集场景下的配置建议,最后分享线程池耗尽的排查思路,以及如何通过自定义 ThreadFactory 和监控手段让线程池更可控、更可观测。
2. 线程池核心参数
Java 的 ThreadPoolExecutor 是线程池的核心实现,它的构造函数有七个参数,其中最关键的是以下四个:
- 核心线程数(corePoolSize):线程池中常驻的线程数量。即使线程空闲,只要不超过核心线程数,线程也不会被回收。
- 最大线程数(maximumPoolSize):线程池允许创建的最大线程数量。当任务队列已满且当前线程数小于最大线程数时,会继续创建新线程。
- 任务队列(workQueue):用于存放等待执行任务的阻塞队列。当线程数达到核心线程数后,新任务会先进入队列排队。
- 拒绝策略(handler):当任务队列已满且线程数已达到最大线程数时,对新提交任务的处理策略。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
workQueue, // 任务队列
threadFactory, // 线程工厂
handler // 拒绝策略
);
2.1 核心线程数与最大线程数
核心线程数是线程池的「保底兵力」,最大线程数是线程池的「兵力上限」。两者之间的差值决定了线程池在高峰期能临时扩充多少线程。
需要特别注意的是:只有当任务队列已满时,线程池才会创建超过核心线程数的线程。如果队列没有满,即使最大线程数设置得很大,线程池也只会保持核心线程数规模的线程在工作。
2.2 任务队列
任务队列的选择直接影响线程池的行为。常见的队列有以下几种:
- LinkedBlockingQueue:无界队列(默认容量为 Integer.MAX_VALUE)。使用无界队列时,maximumPoolSize 参数将失去意义,因为任务永远不会触发队列满,也就不会创建超过核心线程数的线程。
- ArrayBlockingQueue:有界队列,需要指定容量。队列满后才会触发扩容线程的逻辑。
- SynchronousQueue:不存储任务的队列,每个任务必须立即交给一个线程执行,否则就阻塞或触发拒绝策略。适合任务量小但执行快的场景。
2.3 拒绝策略
当队列已满且线程数达到最大值时,新提交的任务会触发拒绝策略。JDK 提供了四种内置策略:
- AbortPolicy(默认):直接抛出 RejectedExecutionException,中断任务的提交。
- CallerRunsPolicy:不抛异常,而是由提交任务的线程自己执行该任务。这能起到一定的「背压」作用,减缓任务提交速度。
- DiscardPolicy:静默丢弃任务,不抛异常也不执行。
- DiscardOldestPolicy:丢弃队列中最旧的任务,然后重新尝试提交当前任务。
3. CPU 密集 vs IO 密集怎么配
线程池的核心线程数没有「银弹」公式,需要根据任务类型来估算。
3.1 CPU 密集任务
CPU 密集任务的特点是:任务几乎不等待,一直在占用 CPU 计算。此时线程数不宜过多,否则线程切换反而会降低吞吐。
推荐配置:核心线程数 ≈ CPU 核数(或核数 + 1)。
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cpuCores,
cpuCores,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1000)
);
3.2 IO 密集任务
IO 密集任务的特点是:任务大部分时间在等待 IO(网络、磁盘、数据库),CPU 利用率不高。此时可以配置更多的线程,让等待期间其他线程继续执行。
推荐配置:核心线程数 ≈ CPU 核数 × 2(或按公式 CPU 核数 / (1 – 阻塞系数) 估算,阻塞系数通常取 0.8~0.9)。
int cpuCores = Runtime.getRuntime().availableProcessors();
int ioThreads = cpuCores * 2;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
ioThreads,
ioThreads * 2,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500)
);
3.3 配置要点
- 队列容量要结合任务峰值:队列太小容易触发拒绝策略,太大则可能导致任务积压、内存膨胀。
- 最大线程数要留有余量:避免高峰期线程数打满后新任务直接被拒绝。
- 拒绝策略要按业务选:对可靠性要求高的任务,建议用 CallerRunsPolicy 或自定义策略;对可丢弃的日志类任务,可以用 DiscardPolicy。
4. 线程池耗尽排查
线程池「爆了」通常表现为:任务被拒绝、接口超时、线程数持续打满。排查思路如下:
4.1 看监控指标
先确认线程池的实时状态:
- 当前活跃线程数:是否长期接近 maximumPoolSize。
- 队列积压量:队列中堆积的任务数量是否持续增长。
- 拒绝次数:是否频繁触发拒绝策略。
4.2 定位任务来源
通过日志或链路追踪,找到提交任务最多的调用方。常见原因包括:
- 上游接口并发突增,超出线程池承载能力。
- 某个任务执行过慢(如慢 SQL、远程调用超时),导致线程被长期占用。
- 任务内部又提交了子任务,形成递归提交,线程池被「套娃」耗尽。
4.3 检查线程池配置
- 核心线程数是否过小,导致任务长期排队。
- 队列是否无界,导致任务积压但线程数不增长。
- keepAliveTime 是否过短,导致空闲线程被频繁回收和重建。
4.4 常见修复手段
- 调大核心线程数或最大线程数。
- 改用有界队列并设置合理的容量。
- 优化任务执行逻辑,缩短单任务耗时。
- 对任务做熔断或限流,避免瞬时洪峰打垮线程池。
5. 自定义 ThreadFactory 和监控
5.1 自定义 ThreadFactory
默认的线程工厂创建的线程名称是 pool-N-thread-M,排查问题时很难定位是哪个业务线程池。自定义 ThreadFactory 可以给线程起有意义的名称,并设置守护线程等属性。
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "order-pool-" + counter.getAndIncrement());
thread.setDaemon(false);
return thread;
}
};
这样在 jstack 或监控平台里,就能一眼看出是哪个业务线程池在占用资源。
5.2 线程池监控
ThreadPoolExecutor 本身提供了几个关键的监控方法:
- getPoolSize():当前线程池中的线程总数。
- getActiveCount():当前活跃线程数。
- getQueue().size():队列中积压的任务数。
- getTaskCount():历史上已提交的任务总数。
- getCompletedTaskCount():已完成的任务总数。
可以封装一个监控工具类,定时采集这些指标并上报到监控系统:
public class ThreadPoolMonitor {
public static void monitor(ThreadPoolExecutor executor, String poolName) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
int active = executor.getActiveCount();
int poolSize = executor.getPoolSize();
int queueSize = executor.getQueue().size();
long completed = executor.getCompletedTaskCount();
System.out.printf("[%s] active=%d, poolSize=%d, queueSize=%d, completed=%d%n",
poolName, active, poolSize, queueSize, completed);
}, 0, 10, TimeUnit.SECONDS);
}
}
5.3 拒绝策略的监控化
自定义拒绝策略时,除了处理拒绝逻辑,还可以记录拒绝次数和任务信息,方便事后排查:
RejectedExecutionHandler handler = (r, executor) -> {
System.err.println("任务被拒绝: " + r.toString());
// 上报到监控系统
MetricsCollector.recordReject(poolName);
throw new RejectedExecutionException("线程池已满");
};
6. 总结
线程池的配置不是一劳永逸的,需要结合业务场景、任务类型和流量峰值持续调优。核心要点可以概括为:
- 核心线程数决定常驻处理能力,最大线程数决定峰值弹性。
- 队列决定任务的缓冲方式,有界队列更可控。
- 拒绝策略决定系统过载时的兜底行为,要按业务可靠性要求选择。
- CPU 密集少配线程,IO 密集多配线程。
- 通过自定义 ThreadFactory 让线程可识别,通过监控指标让线程池状态可观测。
只有把参数、队列、拒绝策略和监控手段结合起来,才能让线程池在高峰期稳如泰山,而不是动不动就「爆掉」。



