在 Java 中,java.util.concurrent.ThreadPoolExecutor 是线程池的核心实现类。掌握其参数配置与运行原理,是应对大厂面试的必备技能。下面从参数详解、工作流程、源码关键逻辑、以及实践配置建议几个方面展开。
一、核心参数详解
java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1. corePoolSize(核心线程数)
- 线程池中保持存活的线程数量,即使空闲也不会被回收(除非设置了
allowCoreThreadTimeOut)。 - 当提交任务时,如果当前线程数 < corePoolSize,会直接创建新线程执行任务(即使有空闲线程也优先创建新线程)。
2. maximumPoolSize(最大线程数)
- 线程池允许创建的最大线程数。
- 当任务队列满了且当前线程数 < maximumPoolSize 时,会继续创建新线程执行任务。
3. keepAliveTime(空闲存活时间)与 unit(时间单位)
- 当线程数超过 corePoolSize 时,多余的空闲线程等待新任务的最长时间,超过此时间则被终止。
- 如果设置了
allowCoreThreadTimeOut(true),则核心线程也会受此参数影响。
4. workQueue(任务队列)
-
用于存放等待执行的任务的阻塞队列。
-
常见队列:
SynchronousQueue:不存储任务,直接交给线程执行。如果没有空闲线程则创建新线程(常用于无界线程池)。LinkedBlockingQueue:无界队列(默认容量Integer.MAX_VALUE),可能导致内存溢出,且永远不会触发拒绝策略。ArrayBlockingQueue:有界队列,可防止资源耗尽,需配合合理的 maximumPoolSize 使用。PriorityBlockingQueue:支持优先级排序。
5. threadFactory(线程工厂)
- 用于创建线程,可以自定义线程名称、是否为守护线程、优先级等,便于问题排查。
6. handler(拒绝策略)
-
当线程池已关闭,或队列满且线程数已达 maximumPoolSize 时,新提交的任务会触发拒绝策略。
-
内置策略:
AbortPolicy:直接抛出RejectedExecutionException(默认)。CallerRunsPolicy:由提交任务的线程(调用者)自己执行任务。DiscardPolicy:静默丢弃任务,不抛出异常。DiscardOldestPolicy:丢弃队列中最老的任务,然后重试提交当前任务。
二、线程池的工作原理(任务提交流程)
当调用 execute() 或 submit() 提交一个任务时,线程池的处理逻辑如下(以 ThreadPoolExecutor 源码为依据):
text
1. 如果当前运行的线程数 < corePoolSize:
创建新线程执行任务(即使有其他空闲线程)。
2. 否则,尝试将任务加入工作队列:
如果加入成功,等待核心线程或新增线程处理。
注意:加入成功后需要双重检查线程池状态,如果线程池已停止则回滚。
3. 如果加入队列失败(队列已满):
如果当前线程数 < maximumPoolSize:
创建新线程执行任务。
否则:
触发拒绝策略。
关键源码片段(简化版):
java
public void execute(Runnable command) {
int c = ctl.get();
// 1. 当前线程数 < corePoolSize
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2. 尝试入队
if (isRunning(c) && workQueue.offer(command)) {
// 再次检查状态,如果非运行状态则移除任务并拒绝
if (!isRunning(ctl.get()) && remove(command))
reject(command);
// 如果当前线程数为0,则补充一个非核心线程
else if (workerCountOf(ctl.get()) == 0)
addWorker(null, false);
}
// 3. 队列满,尝试创建非核心线程
else if (!addWorker(command, false))
reject(command);
}
三、线程池的状态
线程池内部通过一个 AtomicInteger(ctl)维护两个信息:
- 高3位:线程池状态(
RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED) - 低29位:工作线程数量
状态转换:
- RUNNING:能接收新任务,并处理队列中的任务。
- SHUTDOWN:不接收新任务,但能处理已提交的任务(调用
shutdown())。 - STOP:不接收新任务,不处理队列中的任务,中断正在执行的任务(调用
shutdownNow())。 - TIDYING:所有任务已终止,
workerCount为 0,将执行terminated()钩子方法。 - TERMINATED:
terminated()执行完毕。
四、线程池的线程复用原理
线程池中的线程被封装为 Worker 对象,它继承自 AQS 并实现了 Runnable。每个 Worker 会循环从 workQueue 中获取任务并执行:
java
final void runWorker(Worker w) {
Runnable task = w.firstTask;
w.firstTask = null;
while (task != null || (task = getTask()) != null) {
// 执行任务前加锁(用于中断检测)
task.run();
task = null;
}
// 线程退出(空闲超时或被回收)
}
getTask() 方法中会根据当前线程数、超时设置等,决定是否从队列中阻塞/超时获取任务。如果超时且线程数超过 corePoolSize,则返回 null 导致线程退出。
五、参数配置实践建议
1. 区分任务类型
- CPU 密集型:线程数一般设为
CPU 核数 + 1。核心线程数 = 最大线程数,队列可选有界队列(如ArrayBlockingQueue)。 - IO 密集型:线程数可以设大,比如
2 * CPU 核数。由于 IO 阻塞时间长,线程经常等待,可设置较多线程。
2. 合理使用队列
- 无界队列(如
LinkedBlockingQueue) :可能导致任务堆积过多,内存溢出。适用场景:任务量可控,或对响应时间要求不高。 - 有界队列(如
ArrayBlockingQueue) :配合最大线程数使用,可以触发拒绝策略,防止系统过载。
3. 动态调整
- 美团等大厂在实践中有动态线程池配置方案:通过配置中心动态调整
corePoolSize、maximumPoolSize、队列容量,无需重启服务,以适应流量变化。
4. 拒绝策略选择
AbortPolicy:默认,直接抛异常,适合必须及时感知失败的业务。CallerRunsPolicy:降级,由调用者线程执行,保证任务不丢,但可能阻塞调用方。DiscardPolicy/DiscardOldestPolicy:适合允许丢失部分不重要的日志、监控数据等。
六、常见面试追问点
- 线程池为什么先尝试创建核心线程,而不是先入队?
这是为了提高响应速度,如果核心线程有空闲就直接执行,避免任务排队等待。 corePoolSize为 0 会怎样?
当corePoolSize = 0时,提交第一个任务会直接创建线程吗?源码中如果corePoolSize = 0,则第一个任务不会立即创建线程,而是先尝试入队。若队列为空,则创建线程执行。最终线程池中会存在一个线程直到空闲超时。- 如何合理估算线程池大小?
一般公式:线程数 = Ncpu * (1 + 平均等待时间 / 平均计算时间)。在实践中可通过压测逐步调整。