Appearance
thread-pool-basics
1. 实验目标与工程要点
本实验旨在深入解析 Java ThreadPoolExecutor 的 7 大核心参数配置逻辑、线程池生命周期流转状态,以及不同拒绝策略在并发临界条件下的运行行为,帮助开发者掌握 Android 与 Java 后台线程池治理的核心技能。
7 大核心参数关系解构
任务处理顺序(新任务提交时,按以下顺序逐级尝试;当前级「满」才进入下一级):
| 顺序 | 阶段 | 做什么 | 何时进入下一阶段 |
|---|---|---|---|
| 1 | Core Threads | 优先创建或使用核心线程直接执行 | 核心线程都在忙 |
| 2 | Work Queue | 任务进入阻塞队列等待 | 队列已满 |
| 3 | Maximum Threads | 创建非核心线程,扩到 maximumPoolSize | 线程数已达上限且队列仍满 |
| 4 | RejectedExecutionHandler | 按拒绝策略处理(抛异常 / 丢弃 / 调用方执行等) | — |
corePoolSize(核心线程数):常驻线程数量,即便空闲也不会被销毁(除非设置allowCoreThreadTimeOut)。maximumPoolSize(最大线程数):线程池允许创建的最大线程上限。keepAliveTime(空闲存活时间):非核心线程(或设置超时后的核心线程)空闲等待新任务的最长时间。unit(时间单位):keepAliveTime的时间单位(如TimeUnit.SECONDS)。workQueue(任务阻塞队列):用于保存等待执行任务的阻塞队列(如ArrayBlockingQueue、LinkedBlockingQueue)。threadFactory(线程工厂):用于自定义创建线程的工厂(建议自定义命名,方便在 Logcat / Crash 堆栈中排查线程来源)。handler(拒绝策略):当队列已满且活跃线程数已达到maximumPoolSize时触发的拒绝处理逻辑。
2. 深入原理与生活浅显比喻
2.1 生活浅显比喻:银行办事大厅
假设你在经营一家银行营业网点:
corePoolSize(核心员工 = 2人):平时在大厅正式窗口坐镇的 2 名正式员工。workQueue(等候区座椅 = 2把):大厅里摆放的 2 把排队等待椅子。maximumPoolSize(最大窗口数 = 4人):银行大厅最多只能开 4 个窗口(含 2 个紧急临时窗口)。handler(保安劝离/退回机制):当 4 个窗口全开满、且 2 把椅子全坐满时,第 7 位顾客进门后的处理规定。
顾客进门办理业务的过程:
- 顾客 1, 2 进门:2 名正式员工立刻在大厅窗口为其办理。
- 顾客 3, 4 进门:正式窗口都在忙,顾客坐到大厅的 2 把排队椅子上等待。
- 顾客 5, 6 进门:椅子坐满了!银行加开 2 个紧急临时窗口(非核心线程)为顾客 5, 6 办理。
- 顾客 7 进门:4 个窗口全满,2 把椅子也全满!大厅彻底装不下,保安触发拒绝策略(如抛出异常拒绝进入,或叫顾客自己去别的网点办理)。
2.2 线程池 5 大生命周期状态流转
RUNNING:能接收新任务,也能处理队列中的任务。SHUTDOWN:不接收新任务,但继续处理队列中已保存的任务。STOP:不接收新任务,不处理队列中的任务,且打断正在执行的任务。TIDYING:所有任务已终止,工作线程数为 0,即将调用terminated()钩子。TERMINATED:terminated()方法执行完毕后的最终状态。
2.3 shutdown() 与 shutdownNow() 的差异
两者都是「关门不接新任务」,差别在已提交的任务怎么处理:
shutdown() | shutdownNow() | |
|---|---|---|
| 新任务 | ❌ 拒绝 | ❌ 拒绝 |
| 队列里等待的任务 | ✅ 继续执行 | ❌ 清出队列,作为 List<Runnable> 返回 |
| 正在执行的任务 | ✅ 默认跑完(不 interrupt) | ⚠️ 对 worker 线程 interrupt() |
| 进入状态 | SHUTDOWN | STOP |
| 气质 | 优雅关停 | 紧急刹车 |
生活比喻:shutdown() = 银行打烊,不再进新客户,但大厅里已排队、窗口正在办的业务照常办完;shutdownNow() = 立刻清场,排队的人请走,正在办的也尽量打断。
推荐用法:默认用 shutdown();超时仍停不下来时再 shutdownNow() 兜底。
java
pool.shutdown();
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
pool.shutdownNow(); // 超时后硬停
}何时用哪个:
shutdown():进程/模块正常收尾,希望已提交的压缩、写盘、上传尽量做完。shutdownNow():用户取消、页面销毁且任务已无意义,可丢队列、可打断正在跑的。
Android 注意:
- 业务层更常见协程
cancel()/WorkManager,手写shutdown()多见于自建线程池或底层库。 - 不要在主线程长时间
awaitTermination(),会 ANR。 shutdownNow()要配合任务内检查InterruptedException/Thread.interrupted(),否则可能停不干净。
2.4 Executors 工厂方法 vs 显式 ThreadPoolExecutor
newFixedThreadPool / newCachedThreadPool 底层都是 ThreadPoolExecutor,只是工厂方法把危险参数藏起来了:
| 工厂方法 | 等价参数(简化) | 高峰时行为 | 典型风险 | Android 建议 |
|---|---|---|---|---|
newFixedThreadPool(n) | core=max=n + LinkedBlockingQueue(默认近无界) | 线程数固定,任务无限排队 | 队列堆积 → 延迟爆炸 / OOM | ❌ 避免裸用 |
newCachedThreadPool() | core=0, max=Integer.MAX_VALUE + SynchronousQueue | 不排队,优先扩线程 | 线程暴涨 → CPU 抖 / OOM | ❌ 避免裸用 |
显式 ThreadPoolExecutor | 自己设 core/max/队列/拒绝策略 | 可预期、可背压 | 需调参 | ✅ 推荐 |
用例对照:
| 场景 | 不要用 | 推荐 |
|---|---|---|
| 图片压缩 / 解码 | newCachedThreadPool | 有界队列 + AbortPolicy(满则拒绝,业务重试);备选见 createImageCompressPoolWithCallerRuns() |
| 批量离线上传 | newFixedThreadPool(无界队列) | IO 池 + ArrayBlockingQueue + AbortPolicy + 业务重试 |
| 日志 / 埋点写盘 | 多线程写同一文件 | 单线程池 + DiscardPolicy |
| 列表缩略图 / 实时预览 | 无界堆积旧任务 | 小队列 + DiscardOldestPolicy |
可运行对比与 Android 预设见:AndroidThreadPoolPresets.java
3. Android / 移动端实战场景与误配事故
3.1 常见并发拒绝策略对比
| 拒绝策略 | 运行行为(通俗解释) | 吞吐性能 | 运行开销 | 适用 Android 场景与注意事项 |
|---|---|---|---|---|
AbortPolicy (默认) | 直接报错:抛出 RejectedExecutionException 异常。 | ⚡ 极高(立即失败) | 📉 极低 | 适合核心业务任务,结合 try-catch 捕获异常,做兜底降级处理。 |
CallerRunsPolicy | 退回主线程同步跑:由提交任务的调用者线程(如 main 线程)亲自执行该任务。 | 🐢 较差(阻塞提交线程) | 📈 较高(直接侵入调用者上下文) | ⚠️ Android 死坑:如果 UI 主线程提交了耗时任务并触发此策略,主线程会被直接卡死引发 ANR! |
DiscardPolicy | 默默丢弃:静默丢弃最新提交的任务,不抛异常也不执行。 | ⚡ 极高 | 📉 极低 | 适合非核心埋点上报、日志日志刷新等丢失无影响的场景。 |
DiscardOldestPolicy | 喜新厌旧:挤掉阻塞队列中最老(排头)的任务,尝试重新提交新任务。 | 🚀 较高 | 📊 中等 | 适合实时性要求高、旧数据无价值的场景(如视频渲染最新帧)。 |
3.2 Android 实战线程池划分规范
在 Android 开发中,切忌裸用 Executors.newCachedThreadPool()(无界线程)或 Executors.newFixedThreadPool()(无界队列)。
下面四种是业务里更常见的显式 ThreadPoolExecutor 预设(源码见 AndroidThreadPoolPresets.java):
| 预设 | 典型场景 | core / max | 队列 | 拒绝策略 | 要点 |
|---|---|---|---|---|---|
| 图片压缩池 | 解码、压缩、加密(用户触发的保存/导出) | ≈核数/2 ~ ≈核数 | ArrayBlockingQueue(8) | AbortPolicy(默认) | 满则拒绝,try-catch 后提示/重试/降级 |
| 图片压缩池(备选) | 同上,提交路径保证全在后台 | 同上 | 同上 | CallerRunsPolicy | createImageCompressPoolWithCallerRuns();主线程提交会 ANR |
| IO 池 | DB、文件、下载后处理 | ≈核数*2 | ArrayBlockingQueue(64) | AbortPolicy | 失败要可见,上层做重试/降级 |
| 串行日志池 | 日志、埋点落盘 | 1 / 1 | ArrayBlockingQueue(200) | DiscardPolicy | 保顺序;高峰可丢低价值日志 |
| 实时预览池 | 缩略图、列表项预览 | 2 / 4 | ArrayBlockingQueue(4) | DiscardOldestPolicy | 旧任务可丢,新任务优先 |
划分原则:
- CPU 密集型(图片解码、JSON 序列化):线程数接近核数,必须有界队列。
- IO 密集型(网络、磁盘):线程可略多,但仍要上限 + 拒绝策略。
- 主线程防护:默认用
AbortPolicy;仅当提交路径全在后台时,才考虑createImageCompressPoolWithCallerRuns()。
4. 实验源码与运行验证
关键源码
| 文件 | 说明 |
|---|---|
| ThreadPoolBasics.java | 7 大参数扩容流程、CallerRunsPolicy 演示 |
| AndroidThreadPoolPresets.java | Executors 工厂陷阱对比 + Android 四种常用线程池预设 |
运行方式
bash
cd labs/java/runtime-concurrency/thread-pool-basics
# 基础:参数与拒绝策略
javac -d out src/ThreadPoolBasics.java
java -cp out ThreadPoolBasics
# Android 预设与 Executors 对比
javac -d out src/AndroidThreadPoolPresets.java
java -cp out AndroidThreadPoolPresets预期控制台运行输出(ThreadPoolBasics)
text
====== 场景 1:核心参数与扩容流程演示 (AbortPolicy 抛出异常) ======
提交任务 1 成功 | 活跃线程数: 2 | 队列任务数: 0
提交任务 2 成功 | 活跃线程数: 2 | 队列任务数: 0
提交任务 3 成功 | 活跃线程数: 2 | 队列任务数: 1
提交任务 4 成功 | 活跃线程数: 2 | 队列任务数: 2
提交任务 5 成功 | 活跃线程数: 3 | 队列任务数: 2
提交任务 6 成功 | 活跃线程数: 4 | 队列任务数: 2
提交任务 7 失败 | 触发 AbortPolicy 拒绝策略(抛出异常)
====== 场景 2:拒绝策略演示 (CallerRunsPolicy 由提交线程直接执行) ======
任务 1 在线程 [caller-runs-worker] 上开始执行...
提交任务 1 完成
提交任务 2 完成
任务 3 在线程 [main] 上开始执行... <-- 注意:任务 3 被退回给调用者线程 (main) 同步执行!
提交任务 3 完成
任务 2 在线程 [caller-runs-worker] 上开始执行...预期控制台运行输出(AndroidThreadPoolPresets,节选)
text
====== 1. Executors 工厂方法底层参数对比 ======
[newFixedThreadPool(2)]
core=2, max=2, queue=LinkedBlockingQueue(默认近无界), keepAlive=0s
→ 固定 n 线程 + 默认 LinkedBlockingQueue(容量约 21 亿)→ 高峰任务堆队列,易 OOM
[newCachedThreadPool()]
core=0, max=2147483647, queue=SynchronousQueue(零容量,不排队), keepAlive=60s
→ core=0, max=Integer.MAX_VALUE + SynchronousQueue → 高峰疯狂建线程,易 OOM
====== 2. newFixedThreadPool:线程不涨、任务进无界队列 ======
提交 5 个慢任务后 | 活跃线程: 2 | 队列堆积: 3 (线程数仍为 2,多出来的全在队列里等)
====== 3. newCachedThreadPool:不排队、优先扩线程 ======
提交 5 个慢任务后 | 池大小: 5 | 活跃线程: 5 | 队列堆积: 0 (SynchronousQueue 不排队,倾向于直接扩线程)
====== 4. Android 推荐:图片压缩池(AbortPolicy,满则拒绝)======
[图片压缩池] core=4, max=8, queue=ArrayBlockingQueue, policy=AbortPolicy
图片压缩池 | 任务 19 被拒绝 (AbortPolicy)
====== 4b. 备选:图片压缩池 + CallerRunsPolicy(仅后台提交路径可用)======
→ 仅后台提交路径可用;默认推荐 createImageCompressPool()(AbortPolicy)5. 对应知识库文档
- 理论主文档:Java 并发模型总览