跳到主要内容

线程池

问题

线程池的核心参数有哪些?工作流程是怎样的?为什么不推荐使用 Executors 创建线程池?

答案

为什么使用线程池?

  1. 降低资源消耗:复用已创建的线程,避免频繁创建/销毁的开销
  2. 提高响应速度:任务到达时直接有线程执行,无需等待线程创建
  3. 提高可管理性:统一管理线程,防止无限制创建导致 OOM
  4. 提供额外功能:定时执行、周期执行、任务队列监控等

ThreadPoolExecutor 核心参数(7 个)

ThreadPoolExecutor 构造函数
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程的空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
) { ... }
参数说明
corePoolSize核心线程数,即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut
maximumPoolSize线程池的最大线程数
keepAliveTime非核心线程空闲超过此时间会被回收
unitkeepAliveTime 的时间单位
workQueue核心线程满后,新任务先进入此队列排队
threadFactory创建线程的工厂,可自定义线程名称、守护线程等
handler队列满且线程数达到最大值时的拒绝策略

工作流程

任务提交的三个阶段
  1. 核心线程:线程数 < corePoolSize → 创建新线程(即使有空闲线程)
  2. 排队:核心线程满 → 放入 workQueue
  3. 扩容:队列满 → 创建非核心线程(直到 maximumPoolSize)
  4. 拒绝:线程数达到 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
  • CachedThreadPoolmaximumPoolSize = 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
TERMINATEDterminated() 方法执行完毕

优雅关闭线程池

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 密集型Ncpu+1N_{cpu} + 1减少上下文切换
IO 密集型2×Ncpu2 \times N_{cpu}Ncpu/(1阻塞系数)N_{cpu} / (1 - \text{阻塞系数})线程多数时间在等待 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),任务堆积可能 OOM
  • newCachedThreadPoolmaximumPoolSize 为 Integer.MAX_VALUE,可能创建过多线程导致 OOM

应该手动 new ThreadPoolExecutor(),明确每个参数,尤其是有界队列和合理的 maximumPoolSize。

Q4: 线程池的拒绝策略有哪些?如何选择?

答案

4 种内置策略:

  • AbortPolicy:抛异常(默认,适合必须处理的任务)
  • CallerRunsPolicy:调用者线程执行(不丢弃任务,但会阻塞提交线程,起到限流作用)
  • DiscardPolicy:静默丢弃(适合可丢弃任务如日志)
  • DiscardOldestPolicy:丢弃最老任务(适合保留最新任务)

生产中常用 CallerRunsPolicy(不丢失任务)或自定义策略(记录日志+持久化)。

Q5: execute() 和 submit() 的区别?

答案

对比execute()submit()
所属接口ExecutorExecutorService
参数RunnableRunnable / Callable
返回值voidFuture<?> / Future<T>
异常处理直接抛出,需要 UncaughtExceptionHandler异常被封装在 Future 中,get() 时才抛出

Q6: 如何设置核心线程数?

答案

  • CPU 密集型:核心线程数 = CPU 核数 + 1(+1 是为了某个线程偶发阻塞时有备用)
  • IO 密集型:核心线程数 = CPU 核数 × 2(或用公式 Ncpu×(1+W/C)N_{cpu} \times (1 + W/C),W 为等待时间,C 为计算时间)
  • 混合型:拆分成两个线程池

经验公式只是起点,最终需要通过压测确定。

Q7: 核心线程能否被回收?

答案

默认核心线程不会被回收。但可以通过设置 allowCoreThreadTimeOut(true) 让核心线程也在空闲超过 keepAliveTime 后被回收。

Q8: 线程池中线程抛出异常会怎样?

答案

  • 通过 execute() 提交的任务:异常会导致该线程终止,线程池创建一个新线程替代
  • 通过 submit() 提交的任务:异常被封装在 Future 中,调用 future.get() 时以 ExecutionException 形式抛出,线程不会终止

建议在任务内部用 try-catch 处理异常,避免线程频繁创建。

Q9: shutdown() 和 shutdownNow() 的区别?

答案

对比shutdown()shutdownNow()
新任务不接受不接受
队列中的任务继续执行返回未执行的任务列表
正在执行的任务等待完成尝试中断
线程池状态SHUTDOWNSTOP

相关链接