🏊 线程池
七大参数 · 执行流程 · 四种拒绝策略 · 常见线程池 · 大小设置 · 常见坑
1. ThreadPoolExecutor 的七大参数?
构造参数全解
new ThreadPoolExecutor(
corePoolSize, 1️⃣ 核心线程数:常驻,即使空闲也不回收(allowCoreThreadTimeOut 可改)
maximumPoolSize, 2️⃣ 最大线程数:核心+非核心的上限
keepAliveTime, 3️⃣ 非核心线程空闲存活时间
TimeUnit, 4️⃣ 时间单位
workQueue, 5️⃣ 任务队列:核心线程满后任务先排队
threadFactory, 6️⃣ 线程工厂:命名、守护线程等
handler 7️⃣ 拒绝策略:队列满 + 线程满时
)
执行流程(背下来):
- 提交任务 → 核心线程未满 → 创建核心线程执行
- 核心线程满 → 任务放入队列排队
- 队列满 → 创建非核心线程(最多到 maximumPoolSize)
- 线程全满且队列满 → 执行拒绝策略
🎯 面试要点
- 先排队再扩线程——这是线程池"缓冲优先"的设计(区别于信号量)
- 线程命名规范:custom-线程工厂,排障时 jstack 一眼认出业务线程
- 核心线程的懒启动:首次提交任务才创建(prestartAllCoreThreads 可预启动)
2. 四种拒绝策略?
- AbortPolicy(默认):直接抛
RejectedExecutionException——最严格,让调用方知道任务丢了 - CallerRunsPolicy:由提交任务的线程自己执行——天然限流(提交方被拖慢),且不丢任务,生产常用
- DiscardPolicy:静默丢弃——最危险,任务无声消失
- DiscardOldestPolicy:丢弃队列头(最老任务)再重试提交——适合可丢弃旧数据的场景(如更新缓存)
🎯 面试要点
- 业务上拒绝意味着"过载",建议配套监控报警 + 落库补偿
- 可以自定义 RejectedExecutionHandler(写日志、入死信队列)
3. Executors 提供的常见线程池?为什么不推荐?
- newFixedThreadPool(n):固定 n 线程,无界队列 LinkedBlockingQueue → 任务堆积无限,内存耗尽
- newCachedThreadPool():核心 0、最大 Integer.MAX、SynchronousQueue 直接交接 → 突发任务会创建海量线程,资源耗尽
- newSingleThreadExecutor():单线程无界队列
- newScheduledThreadPool(n):定时任务,无界延迟队列
阿里规约明确禁止 Executors 直接创建:无界队列/无限线程数都是隐患,必须手动 new ThreadPoolExecutor 并明确参数。
推荐的规范写法
// 有界队列 + 命名工厂 + CallerRuns 兜底
private static final ThreadPoolExecutor POOL = new ThreadPoolExecutor(
8, 16, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder() // Guava 或自定义
.setNameFormat("order-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy());
🎯 面试要点
- 阿里规约三句:不用 Executors、用有界队列、给线程命名
- ScheduledThreadPool 底层是 DelayedWorkQueue(堆结构)
4. 线程池大小怎么设置?
- CPU 密集型:≈
CPU 核数 + 1(或 N+1),线程过多反而因上下文切换变慢 - IO 密集型:≈
CPU 核数 × 2或公式CPU 核数 × (1 + 等待时间/计算时间)——IO 等待时线程空闲,可以多放线程让出 CPU 给其他线程 - 核心依据:任务特性(计算/IO 比例)、QPS、响应时间要求(排队积压 = 线程数 × 单任务耗时 - 吞吐)
经验值只是起点,最终要靠压测调整:观察线程利用率(CPU 使用率)和队列积压。
🎯 面试要点
- 区分"调参公式"与"压测验证",答公式 + 补充"需压测"是加分项
- IO 密集任务(DB/Redis/HTTP)为主的后端服务:核心线程 2N~4N 常见(N=核数)
- 动态调整:ThreadPoolExecutor 支持 setCorePoolSize 运行时修改
5. 线程池的常见坑?
- 任务异常被吞:execute 提交的任务抛异常,线程被回收重建(Worker 退出),异常只打印不抛出,调用方无感知 → 建议提交 Runnable 时自己 try-catch,或 submit + Future.get 拿异常
- 线程池关闭:shutdown() 优雅关闭(拒绝新任务、执行完存量);shutdownNow() 中断所有线程并返回未执行任务;应用下线必须优雅关闭
- 核心线程回收:默认 keepAliveTime 只对非核心线程生效;allowCoreThreadTimeOut(true) 后核心也回收
- ThreadLocal 复用:线程池线程复用 → ThreadLocal 值不清理会串到下一个任务(内存泄漏 + 数据串扰)
- 父子线程池 / 嵌套提交:容易死锁(父任务等子任务完成,但子任务排队等空闲线程)
- 队列无界无限堆积:见 Executors 问题
优雅关闭模板
pool.shutdown(); // 停止接收新任务
try {
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
pool.shutdownNow(); // 超时强制中断
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
System.err.println("线程池未能完全停止");
}
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
🎯 面试要点
- execute 丢异常 vs submit 捕获异常:面试官爱问"线程池里任务抛异常会怎样"
- 监控:活跃线程数、队列深度、拒绝次数——阿里建议线程池打点上报