面试官:Java 线程池的核心参数是什么?线程池是怎么工作的?
线程池是 Java 并发编程的核心组件,几乎所有 Java 后端项目都在用它。但很多人只会调
Executors.newFixedThreadPool(),对内部原理一知半解。本文带你彻底搞懂线程池。
一、为什么需要线程池?
直接 new Thread() 有三个核心问题:
- 频繁创建/销毁开销大:线程的创建涉及内核态切换,每次 new Thread 代价不菲
- 线程数量不可控:无限制创建线程会耗尽内存,导致 OOM
- 缺乏统一管理:线程的生命周期、异常处理分散,难以监控
线程池通过复用线程、限制并发数量、统一管理生命周期解决了这三个问题。
二、ThreadPoolExecutor 的七个核心参数
Java 线程池的真正构造函数是 ThreadPoolExecutor,接收七个参数:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
参数一:corePoolSize(核心线程数)
线程池常驻的线程数量。即使线程空闲,也不会被销毁(除非设置了 allowCoreThreadTimeOut(true))。
参数二:maximumPoolSize(最大线程数)
线程池允许创建的最大线程数。当任务队列满了之后,才会创建超过 corePoolSize 的线程,但总数不超过 maximumPoolSize。
参数三 & 四:keepAliveTime + unit(空闲线程存活时间)
超出 corePoolSize 的临时线程,如果空闲时间超过这个值就会被销毁。核心线程不受此影响(默认情况下)。
参数五:workQueue(任务队列)
当核心线程全部繁忙时,新任务会先进入任务队列等待。常用的几种:
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
LinkedBlockingQueue | 无界队列(默认容量 Integer.MAX_VALUE) | newFixedThreadPool 使用,但有 OOM 风险 |
ArrayBlockingQueue | 有界队列,需指定容量 | 推荐用于生产环境,可控 |
SynchronousQueue | 不存储任务,来一个立即交给线程 | newCachedThreadPool 使用 |
PriorityBlockingQueue | 按优先级排序的无界队列 | 需要任务优先级时使用 |
参数六:threadFactory(线程工厂)
用于自定义线程的创建方式,常见用途是给线程命名,方便排查问题:
ThreadFactory namedFactory = new ThreadFactoryBuilder()
.setNameFormat("order-pool-%d")
.build();
参数七:handler(拒绝策略)
当任务队列满 + 线程数达到 maximumPoolSize 时,新任务被拒绝,触发拒绝策略:
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) | 直接抛出 RejectedExecutionException |
CallerRunsPolicy | 由调用者线程(提交任务的线程)直接执行该任务 |
DiscardPolicy | 静默丢弃任务,不报错 |
DiscardOldestPolicy | 丢弃队列中最老的任务,然后尝试重新提交当前任务 |
三、线程池的工作流程
这是面试最爱问的核心逻辑,务必掌握:
提交任务
↓
当前运行线程数 < corePoolSize?
├── YES → 创建核心线程执行任务 ✅
└── NO ↓
任务队列未满?
├── YES → 任务入队等待 ✅
└── NO ↓
当前运行线程数 < maximumPoolSize?
├── YES → 创建非核心线程执行任务 ✅
└── NO → 触发拒绝策略 ❌
用一句话总结:先填满核心线程 → 再塞满队列 → 再扩展到最大线程 → 最后才拒绝。
常见误区:很多人以为"任务来了先检查队列",实际上是先创建核心线程,核心线程满了才入队,队满了才创建非核心线程。
四、线程池的状态机
ThreadPoolExecutor 内部用一个 AtomicInteger 的高 3 位存储线程池状态,低 29 位存储线程数量:
| 状态 | 说明 |
|---|---|
RUNNING | 正常运行,接受新任务,处理队列任务 |
SHUTDOWN | 调用 shutdown() 后,不接受新任务,但处理完队列剩余任务 |
STOP | 调用 shutdownNow() 后,不接受新任务,中断正在执行的任务,清空队列 |
TIDYING | 所有任务已终止,线程数为 0,即将调用 terminated() |
TERMINATED | terminated() 执行完毕 |
五、为什么不推荐使用 Executors 工厂方法?
《阿里巴巴 Java 开发手册》明确规定:禁止使用 Executors 创建线程池,原因如下:
// ❌ 危险!使用无界队列,任务堆积会 OOM
ExecutorService fixed = Executors.newFixedThreadPool(10);
// 底层:new LinkedBlockingQueue<Runnable>(),容量 Integer.MAX_VALUE
// ❌ 危险!线程数无上限,大量请求会创建大量线程,OOM
ExecutorService cached = Executors.newCachedThreadPool();
// 底层:maximumPoolSize = Integer.MAX_VALUE
正确做法:直接使用 ThreadPoolExecutor,明确指定所有参数:
// ✅ 推荐方式
ExecutorService executor = new ThreadPoolExecutor(
10, // corePoolSize
20, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500), // 有界队列,容量 500
new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 调用者执行,反压上游
);
六、线程池参数如何合理设置?
经典公式(仅供参考,实际需根据业务测试调优):
CPU 密集型任务(计算、加密、压缩等):
corePoolSize = CPU核心数 + 1
线程过多反而增加上下文切换开销,+1 是为了在偶尔的内存缺页等情况下保持 CPU 不空闲。
IO 密集型任务(数据库、网络请求等):
corePoolSize = CPU核心数 × (1 + 等待时间/计算时间)
IO 期间线程阻塞,可以有更多线程填满 CPU。如果等待时间是计算时间的 4 倍,则 corePoolSize = CPU核心数 × 5。
实际生产中建议:先设保守值,用压测工具(如 JMeter)不断调整,并开启线程池监控(暴露 Metrics)。
七、常见面试追问
Q:线程池里的线程是如何复用的?
Worker 线程执行完一个任务后,不会立即销毁,而是循环调用 getTask() 从队列里拉取下一个任务。这个 while 循环就是线程复用的关键:
// Worker.run() 的简化逻辑
while (task != null || (task = getTask()) != null) {
task.run();
}
Q:shutdown() 和 shutdownNow() 的区别?
shutdown():温和关闭。不再接受新任务,等待队列中的任务执行完毕后关闭。shutdownNow():强制关闭。中断所有线程(发送 interrupt),返回未执行的任务列表。
Q:如何监控线程池的运行状态?
ThreadPoolExecutor executor = ...;
executor.getPoolSize(); // 当前线程数
executor.getActiveCount(); // 活跃线程数(正在执行任务的)
executor.getQueue().size(); // 队列中等待的任务数
executor.getCompletedTaskCount(); // 已完成任务数
小结
| 参数 | 作用 | 注意事项 |
|---|---|---|
| corePoolSize | 常驻线程数 | 根据任务类型合理设置 |
| maximumPoolSize | 线程上限 | 必须 ≥ corePoolSize |
| workQueue | 缓冲任务 | 生产环境用有界队列 |
| handler | 拒绝策略 | CallerRunsPolicy 可实现反压 |
掌握了线程池的工作原理,不仅能在面试中游刃有余,在生产中排查线程池满、任务堆积等问题时也能快速定位根因。