线程池
本篇定位
并发面试出镜率最高的一篇。主线三件事:为什么要用线程池(复用降低开销、统一管理防资源耗尽)、七个核心参数 + 提交执行流程(必考画图)、为什么禁用 Executors 快捷方法(阿里规约高频题)。参数怎么配、常见队列有哪些,文末给速查。
为什么需要线程池
- 降低资源消耗:线程的创建和销毁都涉及系统调用与内存分配,复用线程摊薄成本;
- 提高响应速度:任务到达直接复用已有线程,省去创建等待;
- 统一管理:防止无限制 new Thread 把系统线程数打爆,还能提供排队、监控、超时等治理能力。
标准创建:ThreadPoolExecutor 七参数
ExecutorService pool = new ThreadPoolExecutor(
2, // corePoolSize 核心线程数:常驻,不回收(默认)
4, // maximumPoolSize 最大线程数:核心+临时上限
60, TimeUnit.SECONDS, // keepAliveTime + unit 临时线程空闲多久回收
new LinkedBlockingQueue<>(100), // workQueue 任务排队队列
Executors.defaultThreadFactory(),// threadFactory 建线程的工厂(自定义名字方便排查)
new ThreadPoolExecutor.AbortPolicy()); // handler 队列满+线程满时的拒绝策略
提交任务后的执行流程(必考画图题)
提交任务
│
▼
核心线程未满?──是──▶ 创建核心线程执行
│ 否
▼
队列没满?────是──▶ 任务入队等待
│ 否
▼
最大线程未满?──是──▶ 创建临时线程执行
│ 否
▼
执行拒绝策略
注意顺序坑:是先入队,队列满了才开临时线程,不是先开满线程再排队——很多人栽在这里。
四种拒绝策略
| 策略 | 行为 |
|---|---|
| AbortPolicy(默认) | 抛 RejectedExecutionException |
| CallerRunsPolicy | 谁提交谁自己执行——天然反压,常用于业务池 |
| DiscardPolicy | 静默丢弃新任务 |
| DiscardOldestPolicy | 丢掉队首最老任务,给新任务腾位 |
为什么禁用 Executors 的快捷方法(阿里规约)
// ❌ 都是坑
Executors.newFixedThreadPool(10); // 队列是无界 LinkedBlockingQueue —— 任务堆积直接 OOM
Executors.newSingleThreadExecutor(); // 同上,无界队列
Executors.newCachedThreadPool(); // 最大线程数是 Integer.MAX_VALUE —— 线程无限增长 OOM
// ✅ 手动 new ThreadPoolExecutor,明确每个参数
参数怎么设(经验值 + 原理)
- CPU 密集型(计算、加解密):核心线程数 ≈ N_CPU + 1;
- IO 密集型(RPC 调用、数据库、文件):核心线程数 ≈ N_CPU × (1 + 等待时间/计算时间),粗略按 2N 起步压测调整;
- 队列:有界!容量按可容忍的积压量定,并配好监控;
- 更科学的做法:压测 + 监控(活跃线程数、队列水位、拒绝次数),参数不是拍出来的。
submit vs execute / 优雅关闭
pool.execute(runnable); // 无返回值,异常直接打印堆栈
Future<String> f = pool.submit(callable); // 有返回值,异常在 get() 时抛出
pool.shutdown(); // 不接新任务,已提交的跑完
pool.awaitTermination(30, TimeUnit.SECONDS); // 等待收尾
pool.shutdownNow(); // 中断正在执行的任务(慎用)
坑:submit 的任务里抛了异常但你不调 get(),异常会被吞掉——日志里什么都看不到。处理:任务内部 try-catch,或包一层异常记录。
其他要点速记
- 核心线程默认不回收,
allowCoreThreadTimeOut(true)可让核心线程也走 keepAlive 回收; - 线程池的线程默认都是非守护线程,主线程退出不影响它们,忘记 shutdown 会导致 JVM 不退出;
- 动态调参:
setCorePoolSize()运行时可改(美团的动态线程池实践就是基于此 + 监控)。
💬 评论