一、 线程池核心:ThreadPoolExecutor 七大参数
面试官最常问的第一题通常是:“请描述一下 ThreadPoolExecutor 的构造参数”。这是理解线程池工作机制的基石。在 线程池面试题讲解 中,这一部分必须烂熟于心。
1. 核心线程数 (corePoolSize)
线程池中长期存活的线程数量,即使它们是空闲的,也不会被销毁(除非设置了 allowCoreThreadTimeOut)。当有新任务提交时,如果当前线程数小于 corePoolSize,则会创建新线程执行任务。
2. 最大线程数 (maximumPoolSize)
线程池允许创建的最大线程数。只有当队列满了,且当前线程数小于 maximumPoolSize 时,才会创建新线程。注意:如果使用的是无界队列,maximumPoolSize 将失效。
3. 非核心线程存活时间 (keepAliveTime)
当线程数大于 corePoolSize 时,空闲线程的最大存活时间。超过这个时间,多余的空闲线程将被销毁,直到线程数回落到 corePoolSize。
4. 时间单位 (unit)
keepAliveTime 的时间单位,如 TimeUnit.SECONDS。
5. 工作队列 (workQueue)
用于保存等待执行的任务的阻塞队列。常见的有:
- ⚡ ArrayBlockingQueue:有界队列,基于数组的先进先出队列。
- ⚡ LinkedBlockingQueue:无界队列(默认容量为Integer.MAX_VALUE),基于链表的先进先出队列。
- ⚡ SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作。
- ⚡ PriorityBlockingQueue:具有优先级的无限阻塞队列。
6. 线程工厂 (threadFactory)
用于创建新线程的工厂。默认情况下,线程由 Executors.defaultThreadFactory() 创建,线程名为 "pool-1-thread-1"。自定义线程工厂可以为线程设置更有意义的名字,便于排查问题。
7. 拒绝策略 (handler)
当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这就是 拒绝策略 的作用。
| 拒绝策略 | 处理方式 | 适用场景 |
|---|---|---|
| AbortPolicy | 直接抛出 RejectedExecutionException 异常 |
默认策略,适用于对数据一致性要求高的场景 |
| CallerRunsPolicy | 由调用线程(提交任务的线程)处理该任务 | 降级处理,减缓任务提交速度,不丢失任务 |
| DiscardPolicy | 直接丢弃任务,不抛异常 | 适用于对任务不敏感、可丢失的场景 |
| DiscardOldestPolicy | 丢弃队列中最老的任务,尝试重新提交当前任务 | 适用于实时性要求高,可牺牲旧任务的场景 |
二、 线程池运行机制深度解析
理解线程池如何分配任务是面试中的难点。我们使用选项卡来展示不同的任务提交场景。
核心线程优先创建
当提交一个新任务时,如果当前线程数 < corePoolSize,线程池会创建一个新的核心线程来执行任务,即使此时其他核心线程是空闲的。这是为了应对突发流量,保证核心处理能力。
// 伪代码逻辑
if (workerCount < corePoolSize) {
createCoreThread();
} else {
// 进入队列
}
队列满后创建非核心线程
如果当前线程数 >= corePoolSize,新任务会被放入 workQueue 中。如果队列是有界的且满了,线程池会尝试创建非核心线程(只要当前线程数 < maximumPoolSize)。
else if (!workQueue.offer(task)) {
if (workerCount < maximumPoolSize) {
createNonCoreThread();
} else {
// 触发拒绝策略
reject(task);
}
}
拒绝策略触发
当线程数达到 maximumPoolSize 且队列也满了时,线程池无法再处理新任务,此时将调用 RejectedExecutionHandler 的 rejectedExecution 方法。
这是系统过载的信号,通常伴随着日志报警或降级逻辑。
三、 高频面试题:如何正确创建线程池?
在 线程池面试题讲解 中,Executors 工具类的使用是一个巨大的陷阱。面试官会问:“Executors.newFixedThreadPool 有什么隐患?”
1. 阿里开发手册禁止使用的线程池创建方式
阿里巴巴 Java 开发手册明确规定:线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
2. 隐患分析
- ⚠️ newFixedThreadPool / newSingleThreadExecutor:允许请求的队列长度为
Integer.MAX_VALUE,可能堆积大量请求,导致 OOM (OutOfMemoryError)。 - ⚠️ newCachedThreadPool:允许创建的线程数量为
Integer.MAX_VALUE,可能创建大量线程,导致 OOM。
3. 正确的创建姿势
手动创建 ThreadPoolExecutor,并根据业务类型合理设置参数:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy() // 安全拒绝策略
);
4. 核心线程数设置指南
此类任务计算量大,线程上下文切换开销相对较小。建议设置线程数为 CPU 核心数 + 1,即 N + 1。这样可以最大限度地减少线程切换,提高 CPU 利用率。
此类任务大部分时间在等待 IO 操作(如数据库查询、HTTP 请求),CPU 利用率低。建议设置线程数为 CPU 核心数 2,或者根据并发量适当增加,如 2N 或 N / (1 - 阻塞系数)。
可以根据任务的性质拆分,或者通过监控线程池状态动态调整参数。
四、 进阶:线程池源码中的关键方法
对于高级开发岗位,面试官可能会深入询问源码。以下是 execute() 方法的核心逻辑梳理。
execute() 方法执行流程
- 如果当前运行的线程少于
corePoolSize,则创建新线程来执行任务。 - 如果运行的线程等于或多于
corePoolSize,则将任务加入workQueue。 - 如果无法将任务加入
workQueue(队列已满),则检查当前运行的线程数是否少于maximumPoolSize。 - 如果少于,则创建新线程执行任务。
- 如果多于,则执行拒绝策略。
shutdown() vs shutdownNow()
- ⚙️ shutdown():启动有序关闭,先前提交的任务会执行,但新任务不会被接受。
- ⚙️ shutdownNow():尝试停止所有正在执行的任务,暂停等待处理的任务,并返回等待执行的任务列表。
注意:shutdownNow() 并不能保证立即停止正在执行的任务,它只是发送中断信号 (interrupt())。任务内部需要响应中断才能真正停止。
六、 总结
通过本页面关于 线程池面试题讲解 的详细梳理,相信您已经对 Java 线程池有了全面的认识。从七大参数的配置,到运行流程的把控,再到拒绝策略的选择和源码级的理解,每一个环节都是面试中的得分点。同时,结合实际业务场景选择合适的线程池配置,避免 OOM 和线程泄漏,是区分初级与高级开发者的关键。
希望这份攻略能帮助您在面试中脱颖而出,顺利拿到心仪的 Offer!