线程池高频面试题(核心原理+配置实践+常见误区)
线程池高频面试题(核心原理+配置实践+常见误区)
🔧 一、线程池核心参数与执行流程
1. 线程池的7个核心参数是什么?
corePoolSize
:核心线程数(长期维持的最小线程数,即使空闲也不会销毁,除非开启核心线程超时回收)。maximumPoolSize
:最大线程数(核心线程 + 临时线程的总上限)。keepAliveTime
:非核心线程的空闲存活时间,超时后会被销毁。unit
:keepAliveTime
的时间单位(如TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
)。workQueue
:任务队列(存储核心线程已满时等待执行的任务,需指定有界/无界)。threadFactory
:线程工厂(定制线程名称、优先级、是否为守护线程等,便于问题排查)。handler
:拒绝策略(当线程数达上限且队列满时,对新提交任务的处理方式)。
2. 线程池的执行流程(四步机制)?
- 提交任务后,优先判断核心线程是否空闲:若核心线程未满,直接创建核心线程执行任务;
- 若核心线程已满,判断任务队列是否未满:若队列有空间,将任务存入队列等待;
- 若队列已满,判断当前线程数是否小于
maximumPoolSize
:若是,创建临时线程执行任务; - 若线程数已达
maximumPoolSize
且队列满,触发拒绝策略处理新任务。
⚠️ 二、拒绝策略(4种内置策略)
策略类名 | 核心行为 | 适用场景 |
---|---|---|
AbortPolicy | 直接抛出 RejectedExecutionException 异常(默认策略) | 核心业务不允许任务丢失,需感知异常并处理(如支付、订单创建系统)。 |
CallerRunsPolicy | 由提交任务的线程(如主线程)亲自执行任务,减缓任务提交速度 | 非核心业务,允许同步降级(如日志打印、非实时统计)。 |
DiscardPolicy | 静默丢弃新提交的任务,不抛异常、不反馈 | 允许数据丢失的非关键任务(如用户行为上报、非核心监控数据)。 |
DiscardOldestPolicy | 丢弃队列中最旧的任务(队首任务),然后重试提交当前任务 | 优先处理最新任务,旧任务可丢弃(如实时数据处理、消息推送)。 |
🧠 三、线程池配置原则
1. 如何合理设置线程池大小?
线程池大小需根据任务类型(CPU密集/IO密集)决定,核心是避免“上下文切换过载”或“CPU空闲浪费”:
- CPU密集型任务(如计算、排序):线程数 ≈ CPU核心数 + 1
(理由:CPU核心数已能满负荷计算,+1是为了应对偶尔的线程阻塞,避免CPU空闲)。
示例:4核CPU → 线程数设为5。 - IO密集型任务(如DB查询、网络请求):线程数 ≈ CPU核心数 × 2
(理由:IO操作会导致线程等待(如等待DB响应),此时可让其他线程占用CPU,提高利用率)。
示例:4核CPU → 线程数设为8。 - 混合型任务:拆分为独立的CPU密集子任务和IO密集子任务,分别创建线程池,避免互相影响。
2. 为什么避免使用 Executors
创建线程池?
Executors
提供的快捷创建方法(如 newFixedThreadPool
)存在隐性风险,生产环境不推荐使用,推荐手动创建 ThreadPoolExecutor
:
Executors 方法 | 问题根源 | 风险点 |
---|---|---|
newFixedThreadPool | 使用无界队列 LinkedBlockingQueue | 任务堆积导致内存溢出(OOM) |
newSingleThreadExecutor | 同上 | 同上 |
newCachedThreadPool | 最大线程数为 Integer.MAX_VALUE | 线程数暴增导致OOM |
推荐手动创建示例(显式设置有界队列和拒绝策略):
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class ThreadPoolDemo {public static void main(String[] args) {// 手动创建线程池(7个参数全显式配置)ThreadPoolExecutor executor = new ThreadPoolExecutor(2, // corePoolSize:核心线程数4, // maximumPoolSize:最大线程数60, // keepAliveTime:非核心线程空闲时间TimeUnit.SECONDS, // unit:时间单位new ArrayBlockingQueue<>(100), // workQueue:有界队列(容量100)r -> { // threadFactory:自定义线程工厂(命名线程)Thread thread = new Thread(r);thread.setName("biz-thread-" + thread.getId());return thread;},new ThreadPoolExecutor.AbortPolicy() // handler:拒绝策略);}
}
⚙️ 四、高级问题与最佳实践
1. 线程池如何实现线程复用?
核心是通过 Worker
线程(线程池内部维护的工作线程)的循环取任务机制:
Worker
线程创建后,会调用runWorker()
方法,内部通过getTask()
从任务队列循环获取任务;- 执行完一个任务后,不会销毁线程,而是继续调用
getTask()
获取下一个任务; - 只有当
getTask()
返回null
(如线程池关闭、核心线程超时回收)时,Worker
线程才会终止,实现线程复用。
2. shutdown()
与 shutdownNow()
的区别?
方法名 | 核心行为 | 返回值 | 适用场景 |
---|---|---|---|
shutdown() | 平滑关闭:不接收新任务,等待队列中已存在的任务执行完毕后,再终止线程池 | void | 正常停机(如服务优雅下线) |
shutdownNow() | 强制关闭:立即中断所有正在执行的任务,清空队列,返回未执行的任务列表 | List | 紧急停机(如服务故障恢复) |
代码示例:
// 1. shutdown() 平滑关闭
executor.shutdown();
// 判断线程池是否已终止(可配合 awaitTermination 等待)
while (!executor.isTerminated()) {TimeUnit.MILLISECONDS.sleep(100);
}
System.out.println("线程池已平滑关闭");// 2. shutdownNow() 强制关闭
List<Runnable> unExecutedTasks = executor.shutdownNow();
System.out.println("未执行的任务数:" + unExecutedTasks.size());
System.out.println("线程池已强制关闭");
3. 生产环境线程池优化建议
(1)动态调参
线程池支持运行时调整核心参数,无需重启服务,应对流量波动:
// 运行时增加核心线程数(如从2调整为3)
executor.setCorePoolSize(3);
// 运行时增加最大线程数(如从4调整为5)
executor.setMaximumPoolSize(5);
// 运行时调整非核心线程空闲时间
executor.setKeepAliveTime(30, TimeUnit.SECONDS);
(2)关键监控指标
需监控线程池状态,及时发现任务堆积或线程不足问题:
// 1. 活跃线程数(当前正在执行任务的线程数)
int activeCount = executor.getActiveCount();
// 2. 队列剩余任务数(判断是否堆积)
int queueSize = executor.getQueue().size();
// 3. 已完成任务总数(评估线程池吞吐量)
long completedTaskCount = executor.getCompletedTaskCount();
// 4. 线程池当前总线程数(核心+临时)
int poolSize = executor.getPoolSize();System.out.printf("活跃线程数:%d,队列剩余:%d,已完成任务:%d,总线程数:%d%n",activeCount, queueSize, completedTaskCount, poolSize);
(3)异常处理
线程池任务异常若未捕获,可能导致任务“无声失败”,需两种处理方式:
-
任务内手动捕获异常(局部处理):
executor.execute(() -> {try {// 业务逻辑int result = 1 / 0; // 模拟异常} catch (Exception e) {// 局部异常处理(如日志打印、告警)System.err.println("任务执行异常:" + e.getMessage());} });
-
全局异常处理器(通过
ThreadFactory
配置):// 自定义全局未捕获异常处理器 Thread.UncaughtExceptionHandler exceptionHandler = (t, e) -> {System.err.printf("线程 %s 发生未捕获异常:%s%n", t.getName(), e.getMessage());// 可选:发送告警(如钉钉、短信) };// 配置线程工厂,绑定全局异常处理器 ThreadFactory threadFactory = r -> {Thread thread = new Thread(r);thread.setName("biz-thread-" + thread.getId());thread.setUncaughtExceptionHandler(exceptionHandler); // 绑定处理器return thread; };// 创建线程池时使用该工厂 ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),threadFactory,new ThreadPoolExecutor.AbortPolicy() );
🛠️ 五、常见误区与面试陷阱
误区1:核心线程默认不可回收
真相:核心线程默认不会因空闲被回收,但可通过 allowCoreThreadTimeOut(true)
开启核心线程超时回收,适用于流量波动大的场景(如秒杀后核心线程空闲)。
代码示例:
// 开启核心线程超时回收(空闲60秒后销毁核心线程)
executor.allowCoreThreadTimeOut(true);
executor.setKeepAliveTime(60, TimeUnit.SECONDS);
误区2:submit()
和 execute()
区别
两者都是提交任务的方法,但核心差异在返回值和异常处理:
方法名 | 返回值 | 异常处理方式 |
---|---|---|
execute() | void | 任务内异常需手动 try-catch ,否则会被线程池默认忽略(仅打印日志) |
submit() | Future<?> | 任务异常不会直接抛出,需通过 Future.get() 获取(抛出 ExecutionException ) |
代码示例:
// 1. execute():无返回值,异常需手动捕获
executor.execute(() -> {try {System.out.println("execute() 执行任务");int i = 1 / 0; // 模拟异常} catch (ArithmeticException e) {System.err.println("execute() 捕获异常:" + e.getMessage()); // 必须手动捕获}
});// 2. submit():有返回值,异常通过 Future.get() 获取
Future<?> future = executor.submit(() -> {System.out.println("submit() 执行任务");return 1 / 0; // 模拟异常(不会直接抛出)
});
try {future.get(); // 获取结果时,抛出 ExecutionException
} catch (InterruptedException e) {Thread.currentThread().interrupt();
} catch (ExecutionException e) {// 提取原始异常(e.getCause())System.err.println("submit() 捕获异常:" + e.getCause().getMessage());
}
陷阱题:SynchronousQueue
队列的作用?
核心特点:一个没有容量的阻塞队列,每个插入操作(put)必须等待一个移除操作(take)完成,反之亦然,即“生产一个、消费一个”,不会存储任务。
适用场景:适合短期、高频的任务,避免任务堆积(如 Executors.newCachedThreadPool
底层就是 SynchronousQueue
)。
代码示例:
// 使用 SynchronousQueue 的线程池(类似 CachedThreadPool)
ThreadPoolExecutor cachedExecutor = new ThreadPoolExecutor(0, // corePoolSize=0(无核心线程)Integer.MAX_VALUE, // 最大线程数极大(应对突发短期任务)60, // 临时线程空闲60秒后销毁TimeUnit.SECONDS,new SynchronousQueue<>(), // 无容量队列,插入需等待消费Executors.defaultThreadFactory()
);
💎 总结
面试核心考点:执行流程+拒绝策略+配置实践,需结合业务场景作答(如电商订单处理用“有界队列+AbortPolicy”,避免订单丢失;日志上报用“DiscardPolicy”,允许偶尔丢失)。
关键避坑点:不使用 Executors
,手动创建 ThreadPoolExecutor
并显式配置有界队列和拒绝策略;重视异常处理和运行时监控,避免“任务无声失败”或“内存溢出”。
高频追问:如何排查线程池导致的 OOM?
答:1. 检查任务队列是否为无界(如 LinkedBlockingQueue
未指定容量),改为有界队列;2. 检查最大线程数是否过大(如 Integer.MAX_VALUE
),按任务类型合理设置;3. 通过线程池监控指标(队列大小、活跃线程数)定位任务堆积点;4. 结合 JVM 内存分析工具(如 MAT)排查内存溢出对象是否为任务队列中的任务实例。