前言
线程池是Java并发编程中最常用的工具之一,但很多开发者只停留在"会用"层面。面试中,面试官往往通过线程池考察你对并发编程的理解深度------参数如何设置?为什么这样设置?拒绝策略如何选择?
本文将深入剖析线程池的七大核心参数、参数设置的核心逻辑以及四种拒绝策略的适用场景。
一、线程池的七大核心参数
ThreadPoolExecutor 是Java线程池的核心实现类,其构造函数包含7个关键参数:
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
1.1 corePoolSize(核心线程数)
定义 :线程池中始终保持存活的线程数,即使这些线程处于空闲状态也不会被销毁(除非设置了 allowCoreThreadTimeOut(true))。
执行逻辑:
- 当提交任务时,如果当前线程数 < corePoolSize,会立即创建新线程执行任务
- 即使有其他空闲线程,也会优先创建新线程,直到达到核心线程数
1.2 maximumPoolSize(最大线程数)
定义:线程池允许创建的最大线程数量,包括核心线程和非核心线程。
执行逻辑:
- 当任务队列已满,且当前线程数 < maximumPoolSize 时,会创建新的非核心线程来执行任务
1.3 keepAliveTime + unit(空闲存活时间)
定义:非核心线程空闲时的存活时间。超过这个时间,空闲的非核心线程会被回收。
特殊点 :如果设置了 allowCoreThreadTimeOut(true),核心线程也会受此参数影响。
1.4 workQueue(阻塞队列)
定义:用于存放等待执行的任务的阻塞队列。
常用队列类型:
| 队列 | 特点 | 适用场景 |
|---|---|---|
LinkedBlockingQueue |
无界队列(默认Integer.MAX_VALUE) | 任务量可控,防止OOM需注意 |
ArrayBlockingQueue |
有界队列,需指定容量 | 任务量稳定,需要精确控制 |
SynchronousQueue |
不存储任务,直接移交 | 需要直接执行的任务,配合无限线程数 |
PriorityBlockingQueue |
支持优先级排序 | 任务有优先级需求 |
1.5 threadFactory(线程工厂)
定义 :用于创建新线程的工厂,默认实现为 Executors.defaultThreadFactory()。
最佳实践:自定义ThreadFactory,设置有意义的线程名称,便于问题排查:
java
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("my-pool-%d")
.setDaemon(true)
.build();
1.6 handler(拒绝策略)
当线程池已关闭、或线程数已达maximumPoolSize且队列已满时,新提交的任务会被拒绝。JDK提供了四种内置策略,下文详细解析。
二、线程池的工作流程
理解参数后,我们来看线程池处理任务的核心流程:
提交任务
↓
当前线程数 < corePoolSize?
├── 是 → 创建核心线程执行任务
└── 否 → 尝试放入队列
↓
队列是否已满?
├── 否 → 成功入队,等待执行
└── 是 → 当前线程数 < maximumPoolSize?
├── 是 → 创建非核心线程执行任务
└── 否 → 触发拒绝策略
关键点 :队列已满后才创建非核心线程,而不是核心线程满了就立即创建。
三、核心参数如何设置?
这是面试中考察深度的关键点。线程池大小的设置需要根据任务类型来判断。
3.1 任务类型分类
① CPU密集型
特点:任务主要消耗CPU资源,如复杂计算、加解密、正则匹配等。
公式 :线程数 = CPU核心数 + 1
原理:CPU密集型任务的瓶颈在CPU,线程过多会导致频繁的上下文切换,反而降低吞吐量。多出的1个线程用于应对页缺失等突发情况。
② I/O密集型
特点:任务大量时间在等待I/O操作,如数据库查询、HTTP调用、文件读写等。
公式 :线程数 = CPU核心数 × (1 + 平均等待时间 / 平均工作时间)
简化版 :线程数 = CPU核心数 × 2
原理:I/O密集型任务在等待期间不占用CPU,可以让更多线程并发执行,提高CPU利用率。
③ 混合型
如果任务同时包含CPU计算和I/O操作,可以将任务拆分为两个线程池分别处理。
3.2 队列大小的设置
- 有界队列:推荐使用,设置合理的容量,防止突发流量导致OOM
- 队列大小参考 :
QPS × 平均响应时间 × 容忍的等待时长
3.3 压测调优
实际工作中 :理论公式只是起点,最终需要通过压测验证。逐步调整参数,观察TPS、响应时间、CPU使用率等指标,找到最优配置。
四、四种拒绝策略详解
当线程池无法处理新任务时,拒绝策略(RejectedExecutionHandler)决定如何处理。
4.1 AbortPolicy(默认策略)
行为 :直接抛出 RejectedExecutionException 异常。
java
public static class AbortPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " + e.toString());
}
}
适用场景:关键业务,任务不允许丢失,失败需要立即感知和处理。
4.2 CallerRunsPolicy
行为:由提交任务的线程(调用者)自己执行该任务。
java
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run(); // 调用线程直接执行
}
}
}
效果 :调用线程执行任务期间,无法继续提交新任务,天然实现了限流降级。
适用场景:对任务执行实时性要求不高,允许一定程度的延迟,且不希望丢失任务。
4.3 DiscardPolicy
行为:静默丢弃当前任务,不抛出任何异常。
java
public static class DiscardPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 什么都不做,直接丢弃
}
}
适用场景:非关键任务,如日志上报、监控数据采集,丢失可接受。
4.4 DiscardOldestPolicy
行为:丢弃队列头部(等待时间最久)的任务,然后重新尝试提交当前任务。
java
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll(); // 丢弃队列头部
e.execute(r); // 重新尝试提交
}
}
}
适用场景:追求最新数据的场景,如实时推荐、最新消息推送,可以丢弃旧数据。
4.5 自定义拒绝策略
实现 RejectedExecutionHandler 接口,可以自定义拒绝逻辑,如记录日志、写入消息队列、发送告警等:
java
public class CustomRejectedPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录日志
log.warn("Task rejected: {}", r.toString());
// 发送告警
alertService.send("线程池任务拒绝");
// 降级处理:存入数据库或MQ
saveToDatabase(r);
}
}
五、常见问题与陷阱
5.1 Executors 工具类的隐患
Executors 提供的快捷方法存在一定风险:
| 方法 | 问题 |
|---|---|
newFixedThreadPool |
使用无界队列 LinkedBlockingQueue,任务堆积可能OOM |
newCachedThreadPool |
最大线程数为 Integer.MAX_VALUE,可能创建过多线程导致OOM |
newSingleThreadExecutor |
同样使用无界队列,单线程处理慢时任务堆积 |
建议 :手动创建 ThreadPoolExecutor,明确指定所有参数。
5.2 核心线程数的动态调整
ThreadPoolExecutor 提供了动态调整方法:
java
// 动态调整核心线程数
executor.setCorePoolSize(10);
// 允许核心线程超时回收
executor.allowCoreThreadTimeOut(true);
5.3 线程池的监控
生产环境建议暴露线程池监控指标:
- 当前线程数
- 活跃线程数
- 队列大小
- 已完成任务数
- 拒绝任务数
六、总结
| 参数 | 作用 | 设置建议 |
|---|---|---|
| corePoolSize | 核心线程数 | CPU密集型:核心数+1;I/O密集型:核心数×2 |
| maximumPoolSize | 最大线程数 | 结合队列大小和业务峰值压测确定 |
| keepAliveTime | 空闲存活时间 | 通常30s~60s |
| workQueue | 阻塞队列 | 优先使用有界队列 |
| handler | 拒绝策略 | 根据业务重要性选择 |
线程池配置没有万能公式,理解参数含义后,结合业务特性、通过压测验证,才能找到最适合的配置。
📌 下一篇预告:Synchronized 与 ReentrantLock 的区别------从底层原理到面试话术。