《Java 100 天进阶之路》第55篇:线程池ThreadPoolExecutor(2026版)
第55篇:线程池ThreadPoolExecutor(2026版)
📌系列导航:《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第54篇:AQS抽象队列同步器 |
➡️ 下一篇:第56篇:死锁与排查工具(待发布)
🗺️ 本文阅读地图(3 分钟速览)
第54篇拿下了 AQS,本篇深入并发编程最常用的工具——线程池。掌握了 ThreadPoolExecutor,你就掌握了 Java 并发编程的“生产力工具”:
| 模块 | 核心问题 | 一句话回答 |
|---|---|---|
| 七大参数 | 线程池由哪些参数控制? | corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler |
| 工作流程 | 任务提交后怎么处理? | 核心线程 → 任务队列 → 非核心线程 → 拒绝策略 |
| 四种拒绝策略 | 线程池满了怎么办? | AbortPolicy(抛异常)、CallerRunsPolicy(调用者执行)、DiscardPolicy(丢弃)、DiscardOldestPolicy(丢弃最旧) |
| Executors 陷阱 | 为什么大厂禁止用 Executors? | 无界队列 → OOM,无限线程 → 资源耗尽 |
| 面试最爱问 | 高频考点有哪些? | 见文末 🎤 小节 |
文章目录
- 第55篇:线程池ThreadPoolExecutor(2026版)
- 🗺️ 本文阅读地图(3 分钟速览)
- 一、核心知识点
- 二、生活类比:从“互联网公司”到“线程池”
- 三、七大核心参数详解
- 3.1 corePoolSize(核心线程数)
- 3.2 maximumPoolSize(最大线程数)
- 3.3 keepAliveTime + unit(空闲存活时间)
- 3.4 workQueue(工作队列)
- 3.5 threadFactory(线程工厂)
- 3.6 handler(拒绝策略)
- 四、任务执行流程(面试核心)
- 4.1 完整流程图
- 4.2 关键理解
- 4.3 execute() vs submit()
- 4.4 线程池生命周期管理
- 五、Executors 的陷阱(为什么大厂禁用)
- 5.1 三大陷阱
- 5.2 正确做法:手动创建 ThreadPoolExecutor
- 六、生产级避坑清单
- 七、面试高频考点
- 面试官追问陷阱(加分题)
- 八、练习题
- 📊 你的学习进度
- 👉 下一篇文章预告
一、核心知识点
线程池是什么?
线程池是一种池化资源管理技术,通过预先创建一定数量的线程并统一管理,实现任务的异步执行与资源复用。
四大核心优势:
| 优势 | 说明 |
|---|---|
| 降低资源消耗 | 复用已创建的线程,避免频繁创建/销毁的系统开销 |
| 提高响应速度 | 任务到达时无需等待线程创建即可执行 |
| 提高线程可管理性 | 统一分配、调优和监控线程资源 |
| 提供附加功能 | 定时执行、并发控制、任务排队等 |
核心设计思想:基于生产者-消费者模型——提交任务的线程是生产者,线程池中的工作线程是消费者,任务队列(BlockingQueue)是缓冲区。
七大核心参数:
publicThreadPoolExecutor(intcorePoolSize,// 1. 核心线程数intmaximumPoolSize,// 2. 最大线程数longkeepAliveTime,// 3. 空闲线程存活时间TimeUnitunit,// 4. 时间单位BlockingQueue<Runnable>workQueue,// 5. 工作队列ThreadFactorythreadFactory,// 6. 线程工厂RejectedExecutionHandlerhandler// 7. 拒绝策略)二、生活类比:从“互联网公司”到“线程池”
线程池就像一家互联网公司的人力资源管理:
- corePoolSize(核心线程数):公司的正式员工。只要公司不倒闭,即使没活干也不会轻易辞退。
- maximumPoolSize(最大线程数):公司能容纳的总人数(正式员工 + 临时工)。当正式员工都在忙,任务积压时,公司会招聘临时工。
- workQueue(任务队列):公司的待办任务池。正式员工忙不过来时,新任务先放进待办池排队。
- 非核心线程(临时工):任务池满了,正式员工忙不过来,公司开始招临时工帮忙。任务高峰期过去,临时工空闲太久就会被裁掉(keepAliveTime)。
- 拒绝策略:正式员工、临时工都在忙,待办任务池也满了,新任务来了怎么办?公司有四种应对方案(见下文)。
线程池初始化时不会立即创建核心线程,而是在任务提交时懒加载创建,正式员工是“入职后才开始干活”。
三、七大核心参数详解
3.1 corePoolSize(核心线程数)
定义:线程池中长期维持的最小线程数量,即使空闲也不会被销毁。
核心行为:
- 默认情况下,核心线程会一直存活
- 提交新任务时,如果当前运行线程数 < corePoolSize,立即创建新线程执行任务(即使其他核心线程空闲)
- 可通过
allowCoreThreadTimeOut(true)允许核心线程超时销毁
设置建议:
- CPU 密集型(计算为主):
CPU 核数 + 1 - IO 密集型(网络/磁盘等待为主):
CPU 核数 * 2或CPU 核数 / (1 - 阻塞系数)
3.2 maximumPoolSize(最大线程数)
定义:线程池允许创建的最大线程数量。
核心行为:
- 当核心线程已满且工作队列已满时,线程池会创建非核心线程直到达到 maximumPoolSize
- 包含核心线程和非核心线程两部分
注意事项:
- maximumPoolSize 必须 >= corePoolSize
- 如果队列是无界队列(如
LinkedBlockingQueue默认),该参数失效(因为队列永远不会满)
3.3 keepAliveTime + unit(空闲存活时间)
定义:非核心线程空闲后的存活时间。
核心行为:
- 当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程在终止前等待新任务的最长时间
- 若设置
allowCoreThreadTimeOut(true),核心线程也会受此参数影响
3.4 workQueue(工作队列)
定义:用于存储等待执行任务的阻塞队列。
常见队列对比:
| 队列类型 | 特点 | 适用场景 | 风险点 |
|---|---|---|---|
| ArrayBlockingQueue | 有界阻塞队列,基于数组,FIFO | 任务量可控,需限制队列长度 | 需合理设置队列大小 |
| LinkedBlockingQueue | 可选有界(默认无界),基于链表 | 流量平稳的后台任务 | 无界队列 → 内存溢出风险 |
| SynchronousQueue | 不存储元素,直接交给线程 | 高吞吐、低延迟场景 | 需配合足够线程数 |
| PriorityBlockingQueue | 支持优先级的无界阻塞队列 | 需按优先级执行任务 | 无界队列 → OOM 风险 |
| DelayQueue | 延迟队列,元素到期后才能被获取 | 定时任务、超时处理 | 无界队列 → OOM 风险 |
生产环境建议:
- 避免使用无界队列,推荐设置队列容量为
max(核心线程数, 预期峰值 QPS × 平均处理时间) - 对于突发流量场景,可采用
SynchronousQueue+ 弹性扩容策略
3.5 threadFactory(线程工厂)
定义:创建线程的工厂。
核心作用:
- 默认是
Executors.defaultThreadFactory() - 自定义时可设置线程名称、是否为守护线程等
自定义示例:
ThreadFactorynamedFactory=r->newThread(r,"biz-pool-%d");// 或使用 GuavaThreadFactorynamedFactory=newThreadFactoryBuilder().setNameFormat("order-process-pool-%d").build();3.6 handler(拒绝策略)
定义:当线程池已满(队列满了 + 线程数已达 maximum)且还有新任务时触发的处理策略。
四种内置拒绝策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy | 抛出RejectedExecutionException(默认) | 需要明确感知任务被拒绝 |
| CallerRunsPolicy | 由调用者线程执行该任务 | 不能丢失任务,可接受调用者阻塞 |
| DiscardPolicy | 静默丢弃新任务,不抛异常 | 可容忍部分任务丢失(如日志) |
| DiscardOldestPolicy | 丢弃队列中最旧的任务,再提交新任务 | 需要最新任务优先执行 |
💡AbortPolicy 是默认策略,如果不想任务丢失,应显式设置
CallerRunsPolicy。
四、任务执行流程(面试核心)
4.1 完整流程图
线程池执行execute()的四步流程:
4.2 关键理解
⚠️重要:队列满了才创建非核心线程,而不是先扩充到 maximumPoolSize。
设计逻辑:核心线程是“常驻员工”,任务是“待办池”,非核心线程是“临时工”。只有待办池满了,正式员工忙不过来,才招临时工。这避免了过度创建线程带来的资源浪费。
边界情况:
- 当
corePoolSize = maximumPoolSize时,退化为固定大小线程池 - 设置
maximumPoolSize为Integer.MAX_VALUE时,允许无限扩容(需谨慎使用) - 无界队列时,
maximumPoolSize参数失效(队列永远不会满)
4.3 execute() vs submit()
| 对比 | execute(Runnable) | submit(Callable/Runnable) |
|---|---|---|
| 返回值 | ❌ 无返回值 | ✅ 返回Future<T> |
| 异常处理 | 任务异常直接抛出到控制台 | 异常封装在 Future 中,调用get()时抛出 |
| 适用场景 | 不需要返回结果的任务 | 需要获取执行结果或处理异常 |
4.4 线程池生命周期管理
| 方法 | 说明 |
|---|---|
shutdown() | 优雅关闭——等待已提交任务完成,不再接受新任务 |
shutdownNow() | 立即关闭——尝试中断运行中任务,返回未执行任务列表 |
isShutdown() | 判断是否已关闭 |
isTerminated() | 判断是否已终止 |
awaitTermination(timeout, unit) | 阻塞等待线程池终止 |
五、Executors 的陷阱(为什么大厂禁用)
阿里巴巴《Java开发手册》明确禁止在生产环境使用Executors工厂方法创建线程池。
5.1 三大陷阱
| Executors 方法 | 隐藏的问题 | 后果 |
|---|---|---|
newFixedThreadPool(n) | 使用无界 LinkedBlockingQueue(容量Integer.MAX_VALUE) | 任务堆积 →OOM |
newSingleThreadExecutor() | 同上,且单线程处理 | 积压风险更高 →OOM |
newCachedThreadPool() | 最大线程数Integer.MAX_VALUE,无限创建线程 | 高并发下创建海量线程 →资源耗尽 / CPU 100% |
newScheduledThreadPool(n) | 同样存在线程数失控风险 | OOM 或资源耗尽 |
⚠️结论:Executors 的便利背后是隐患——“无界队列 + 固定线程 = 任务无限堆积 → OOM”,“无界线程数 = 系统资源被耗尽 → OOM 或 CPU 打满”。
5.2 正确做法:手动创建 ThreadPoolExecutor
ExecutorServiceexecutor=newThreadPoolExecutor(10,// corePoolSize20,// maximumPoolSize60L,TimeUnit.SECONDS,// keepAliveTimenewArrayBlockingQueue<>(100),// ✅ 有界队列,防止OOMnewThreadFactoryBuilder()// ✅ 自定义线程名.setNameFormat("biz-pool-%d").build(),newThreadPoolExecutor.AbortPolicy()// ✅ 明确拒绝策略);六、生产级避坑清单
✅ 线程池生产环境使用规范 1. 禁止使用 Executors 创建线程池 → 手动创建 ThreadPoolExecutor 2. 必须使用有界队列 → ArrayBlockingQueue 或指定容量的 LinkedBlockingQueue 3. 自定义线程名称 → ThreadFactory 设置线程名,便于日志追踪和问题排查 4. 根据场景选择合适的拒绝策略 → 不能丢任务用 CallerRunsPolicy 5. 线程数配置参考: - CPU 密集型:CPU 核数 + 1 - IO 密集型:CPU 核数 * 2 或更高 6. 务必在 finally 中关闭线程池 → 使用 shutdown() + awaitTermination() 7. 任务中必须捕获异常 → 否则异常会丢失,线程被回收七、面试高频考点
Q1:ThreadPoolExecutor 的七大参数是什么?各自的作用?
corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(空闲存活时间)、unit(时间单位)、workQueue(工作队列)、threadFactory(线程工厂)、handler(拒绝策略)。核心线程是“常驻员工”,非核心线程是“临时工”,工作队列是“待办池”。
Q2:任务提交后线程池的执行流程?
三步判断:① 当前线程数 < corePoolSize → 创建核心线程执行;② 队列未满 → 放入队列等待;③ 线程数 < maximumPoolSize → 创建非核心线程执行;④ 否则执行拒绝策略。队列满了才创建非核心线程。
Q3:为什么禁止使用 Executors 创建线程池?
newFixedThreadPool和newSingleThreadExecutor使用无界 LinkedBlockingQueue,任务堆积会导致OOM。newCachedThreadPool最大线程数为Integer.MAX_VALUE,高并发下会创建海量线程,导致系统资源耗尽或 CPU 100%。生产环境必须手动创建 ThreadPoolExecutor,使用有界队列。
Q4:线程池的拒绝策略有哪些?
四种内置策略:① AbortPolicy(抛异常,默认);② CallerRunsPolicy(调用者线程执行,可防止任务丢失);③ DiscardPolicy(静默丢弃);④ DiscardOldestPolicy(丢弃最旧任务)。
Q5:corePoolSize 和 maximumPoolSize 如何设置?
CPU 密集型:corePoolSize = CPU 核数 + 1,减少上下文切换。IO 密集型:corePoolSize = CPU 核数 * 2 或更高,利用等待 IO 的时间片。maximumPoolSize 根据业务峰值和系统资源调整,一般为核心线程数的 2-4 倍。必须配合有界队列,否则 maximumPoolSize 失效。
面试官追问陷阱(加分题)
追问1:“LinkedBlockingQueue无界时,maximumPoolSize还有意义吗?”
👉没有意义。无界队列永远不会满,线程池永远不会创建非核心线程。
maximumPoolSize和拒绝策略都失效,只有 corePoolSize 在工作。这是Executors.newFixedThreadPool()的致命问题。
追问2:“CallerRunsPolicy有什么副作用?什么时候用?”
👉 调用者线程执行任务,会阻塞调用者,可能导致上游系统超时。适用于不能丢失任务的场景(如支付回调、订单确认),且调用者能承受阻塞。
追问3:“线程池中的线程抛异常会怎样?”
👉 任务抛出未捕获异常时,线程会被回收(Worker 退出),线程池会创建新线程补充。这也是为什么需要在任务中自行捕获异常——否则任务失败不会重试,且线程创建销毁有额外开销。
八、练习题
分析题:用
Executors.newFixedThreadPool(10)创建线程池,提交大量任务后为什么会 OOM?💡 思路:
newFixedThreadPool底层使用无界 LinkedBlockingQueue,任务处理速度跟不上提交速度时,任务不断在队列中堆积,最终耗尽堆内存。代码题:手动创建一个线程池,要求:核心线程数 5、最大线程数 20、空闲存活 60 秒、有界队列容量 100、拒绝策略为 CallerRunsPolicy、自定义线程名“biz-pool-%d”。
场景设计:某支付系统需要异步处理订单,任务绝对不能丢失,且系统对延迟不敏感。应该选择什么拒绝策略?为什么?
💡 思路:选择
CallerRunsPolicy。调用者线程直接执行任务,确保任务不会丢失。虽然会阻塞调用者,但对延迟不敏感的系统可以接受。
📊 你的学习进度
- 当前:第55篇 / 共108篇 ·进阶篇:并发编程与JUC详解(第51~60篇)
- ✅ 已完成:基础篇44篇 + 第45~55篇
- 📖 正在学:第55篇
- ⏳ 待学习:第56~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
👉 下一篇文章预告
🚀下一篇:《第56篇:死锁与排查工具》
内容简介:死锁的四大必要条件、
jstack和jconsole排查工具、代码示例、预防与避免策略、生产环境实战案例。
👉多线程专题持续深入,拿下死锁排查!
📌《Java 100 天进阶之路 | 从入门到上岗就业》每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!
