线程池
问题
线程池的核心参数有哪些?工作流程是怎样的?为什么不推荐使用 Executors 创建线程池?
答案
为什么使用线程池?
- 降低资源消耗:复用已创建的线程,避免频繁创建/销毁的开销
- 提高响应速度:任务到达时直接有线程执行,无需等待线程创建
- 提高可管理性:统一管理线程,防止无限制创建导致 OOM
- 提供额外功能:定时执行、周期执行、任务队列监控等
ThreadPoolExecutor 核心参数(7 个)
ThreadPoolExecutor 构造函数
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程的空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
) { ... }
| 参数 | 说明 |
|---|---|
corePoolSize | 核心线程数,即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut) |
maximumPoolSize | 线程池的最大线程数 |
keepAliveTime | 非核心线程空闲超过此时间会被回收 |
unit | keepAliveTime 的时间单位 |
workQueue | 核心线程满后,新任务先进入此队列排队 |
threadFactory | 创建线程的工厂,可自定义线程名称、守护线程等 |
handler | 队列满且线程数达到最大值时的拒绝策略 |
工作流程
任务提交的三个阶段
- 核心线程:线程数 < corePoolSize → 创建新线程(即使有空闲线程)
- 排队:核心线程满 → 放入 workQueue
- 扩容:队列满 → 创建非核心线程(直到 maximumPoolSize)
- 拒绝:线程数达到 maximumPoolSize 且队列满 → 执行拒绝策略
任务队列(BlockingQueue)
| 队列类型 | 特性 | 适用场景 |
|---|---|---|
LinkedBlockingQueue | 无界队列(默认 Integer.MAX_VALUE) | 任务量可预估,不怕堆积 |
ArrayBlockingQueue | 有界队列 | 推荐,限制队列大小防止 OOM |
SynchronousQueue | 不存储任务,直接交给线程 | 吞吐量高,需要大 maximumPoolSize |
PriorityBlockingQueue | 优先级排序 | 任务有优先级 |
DelayQueue | 延时队列 | 定时任务 |
详见 阻塞队列。
四种拒绝策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy(默认) | 抛出 RejectedExecutionException | 重要任务,不能丢弃 |
CallerRunsPolicy | 由提交任务的线程自己执行 | 不允许丢弃,可接受降速 |
DiscardPolicy | 静默丢弃新任务 | 日志等可丢弃的任务 |
DiscardOldestPolicy | 丢弃队列中最老的任务 | 保留最新任务 |
也可以自定义拒绝策略(实现 RejectedExecutionHandler 接口):
CustomRejectedHandler.java
// 自定义拒绝策略:记录日志 + 持久化到数据库
public class LogAndPersistPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录日志
log.warn("任务被拒绝: {}, 队列大小: {}", r, executor.getQueue().size());
// 持久化到数据库,后续补偿执行
taskRepository.save(r);
}
}
Executors 工厂方法(不推荐使用)
Executors 工厂方法
// 1. 固定线程数 —— 无界队列 LinkedBlockingQueue
ExecutorService fixed = Executors.newFixedThreadPool(4);
// 2. 缓存线程池 —— SynchronousQueue + maximumPoolSize = Integer.MAX_VALUE
ExecutorService cached = Executors.newCachedThreadPool();
// 3. 单线程 —— 无界队列
ExecutorService single = Executors.newSingleThreadExecutor();
// 4. 定时线程池
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
为什么不推荐 Executors?(阿里巴巴规范)
- FixedThreadPool / SingleThreadExecutor:使用无界队列
LinkedBlockingQueue,任务堆积可能导致 OOM - CachedThreadPool:
maximumPoolSize = Integer.MAX_VALUE,可能创建大量线程导致 OOM - ScheduledThreadPool:同样使用无界队列
推荐手动创建 ThreadPoolExecutor,明确指定参数:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
线程池状态
线程池有 5 种状态,记录在 ctl 字段的高 3 位:
| 状态 | 含义 |
|---|---|
| RUNNING | 接受新任务,处理队列中的任务 |
| SHUTDOWN | 不接受新任务,继续处理队列中的任务 |
| STOP | 不接受新任务,不处理队列任务,中断正在执行的任务 |
| TIDYING | 所有任务终止,workerCount = 0 |
| TERMINATED | terminated() 方法执行完毕 |
优雅关闭线程池
GracefulShutdown.java
executor.shutdown(); // 1. 停止接受新任务,等待已提交任务完成
try {
// 2. 等待所有任务完成,最多等 60 秒
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 3. 超时则强制关闭
// 4. 再等待一段时间
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
log.error("线程池未能正常关闭");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
线程池参数配置建议
| 任务类型 | 核心线程数建议 | 说明 |
|---|---|---|
| CPU 密集型 | 减少上下文切换 | |
| IO 密集型 | 或 | 线程多数时间在等待 IO |
| 混合型 | 拆分为 CPU 密集和 IO 密集两个池 | 分而治之 |
经验公式只是起点
实际线程数应通过压测确定。关注指标:CPU 利用率、线程池队列长度、任务执行时间、吞吐量。
线程池监控
ThreadPoolMonitor.java
ThreadPoolExecutor pool = ...;
// 核心监控指标
pool.getPoolSize(); // 当前线程数
pool.getActiveCount(); // 活跃线程数
pool.getQueue().size(); // 队列中等待的任务数
pool.getCompletedTaskCount(); // 已完成任务数
pool.getTaskCount(); // 总任务数(已完成 + 执行中 + 排队)
pool.getLargestPoolSize(); // 历史最大线程数
常见面试问题
Q1: 线程池的核心参数有哪些?
答案:
7 个参数:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(空闲存活时间)、unit(时间单位)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。
Q2: 线程池的执行流程?
答案:
提交任务 → 线程数 < corePoolSize 则创建核心线程 → 否则放入 workQueue → 队列满则创建非核心线程(到 maximumPoolSize) → 都满则执行拒绝策略。
注意:新任务先进队列,队列满了才扩容线程。这与直觉中"先扩容线程再排队"不同。
Q3: 为什么不推荐使用 Executors 创建线程池?
答案:
newFixedThreadPool/newSingleThreadExecutor:使用LinkedBlockingQueue(容量 Integer.MAX_VALUE),任务堆积可能 OOMnewCachedThreadPool:maximumPoolSize为 Integer.MAX_VALUE,可能创建过多线程导致 OOM
应该手动 new ThreadPoolExecutor(),明确每个参数,尤其是有界队列和合理的 maximumPoolSize。
Q4: 线程池的拒绝策略有哪些?如何选择?
答案:
4 种内置策略:
AbortPolicy:抛异常(默认,适合必须处理的任务)CallerRunsPolicy:调用者线程执行(不丢弃任务,但会阻塞提交线程,起到限流作用)DiscardPolicy:静默丢弃(适合可丢弃任务如日志)DiscardOldestPolicy:丢弃最老任务(适合保留最新任务)
生产中常用 CallerRunsPolicy(不丢失任务)或自定义策略(记录日志+持久化)。
Q5: execute() 和 submit() 的区别?
答案:
| 对比 | execute() | submit() |
|---|---|---|
| 所属接口 | Executor | ExecutorService |
| 参数 | Runnable | Runnable / Callable |
| 返回值 | void | Future<?> / Future<T> |
| 异常处理 | 直接抛出,需要 UncaughtExceptionHandler | 异常被封装在 Future 中,get() 时才抛出 |
Q6: 如何设置核心线程数?
答案:
- CPU 密集型:核心线程数 = CPU 核数 + 1(+1 是为了某个线程偶发阻塞时有备用)
- IO 密集型:核心线程数 = CPU 核数 × 2(或用公式 ,W 为等待时间,C 为计算时间)
- 混合型:拆分成两个线程池
经验公式只是起点,最终需要通过压测确定。
Q7: 核心线程能否被回收?
答案:
默认核心线程不会被回收。但可以通过设置 allowCoreThreadTimeOut(true) 让核心线程也在空闲超过 keepAliveTime 后被回收。
Q8: 线程池中线程抛出异常会怎样?
答案:
- 通过
execute()提交的任务:异常会导致该线程终止,线程池创建一个新线程替代 - 通过
submit()提交的任务:异常被封装在Future中,调用future.get()时以ExecutionException形式抛出,线程不会终止
建议在任务内部用 try-catch 处理异常,避免线程频繁创建。
Q9: shutdown() 和 shutdownNow() 的区别?
答案:
| 对比 | shutdown() | shutdownNow() |
|---|---|---|
| 新任务 | 不接受 | 不接受 |
| 队列中的任务 | 继续执行 | 返回未执行的任务列表 |
| 正在执行的任务 | 等待完成 | 尝试中断 |
| 线程池状态 | SHUTDOWN | STOP |
相关链接
- ThreadPoolExecutor - Java 17 API
- Executors - Java 17 API
- 线程基础 - 线程创建与生命周期
- 阻塞队列 - 各种 BlockingQueue 实现
- Fork/Join 框架 - 另一种并行任务框架
- OOM 排查 - 线程池 OOM 排查