Skip to content

thread-pool-basics

1. 实验目标与工程要点

本实验旨在深入解析 Java ThreadPoolExecutor 的 7 大核心参数配置逻辑、线程池生命周期流转状态,以及不同拒绝策略在并发临界条件下的运行行为,帮助开发者掌握 Android 与 Java 后台线程池治理的核心技能。

7 大核心参数关系解构

任务处理顺序(新任务提交时,按以下顺序逐级尝试;当前级「满」才进入下一级):

顺序阶段做什么何时进入下一阶段
1Core Threads优先创建或使用核心线程直接执行核心线程都在忙
2Work Queue任务进入阻塞队列等待队列已满
3Maximum Threads创建非核心线程,扩到 maximumPoolSize线程数已达上限且队列仍满
4RejectedExecutionHandler按拒绝策略处理(抛异常 / 丢弃 / 调用方执行等)
  1. corePoolSize(核心线程数):常驻线程数量,即便空闲也不会被销毁(除非设置 allowCoreThreadTimeOut)。
  2. maximumPoolSize(最大线程数):线程池允许创建的最大线程上限。
  3. keepAliveTime(空闲存活时间):非核心线程(或设置超时后的核心线程)空闲等待新任务的最长时间。
  4. unit(时间单位)keepAliveTime 的时间单位(如 TimeUnit.SECONDS)。
  5. workQueue(任务阻塞队列):用于保存等待执行任务的阻塞队列(如 ArrayBlockingQueueLinkedBlockingQueue)。
  6. threadFactory(线程工厂):用于自定义创建线程的工厂(建议自定义命名,方便在 Logcat / Crash 堆栈中排查线程来源)。
  7. handler(拒绝策略):当队列已满且活跃线程数已达到 maximumPoolSize 时触发的拒绝处理逻辑。

2. 深入原理与生活浅显比喻

2.1 生活浅显比喻:银行办事大厅

假设你在经营一家银行营业网点

  • corePoolSize (核心员工 = 2人):平时在大厅正式窗口坐镇的 2 名正式员工。
  • workQueue (等候区座椅 = 2把):大厅里摆放的 2 把排队等待椅子。
  • maximumPoolSize (最大窗口数 = 4人):银行大厅最多只能开 4 个窗口(含 2 个紧急临时窗口)。
  • handler (保安劝离/退回机制):当 4 个窗口全开满、且 2 把椅子全坐满时,第 7 位顾客进门后的处理规定。

顾客进门办理业务的过程:

  1. 顾客 1, 2 进门:2 名正式员工立刻在大厅窗口为其办理。
  2. 顾客 3, 4 进门:正式窗口都在忙,顾客坐到大厅的 2 把排队椅子上等待。
  3. 顾客 5, 6 进门:椅子坐满了!银行加开 2 个紧急临时窗口(非核心线程)为顾客 5, 6 办理。
  4. 顾客 7 进门:4 个窗口全满,2 把椅子也全满!大厅彻底装不下,保安触发拒绝策略(如抛出异常拒绝进入,或叫顾客自己去别的网点办理)。

2.2 线程池 5 大生命周期状态流转

  • RUNNING:能接收新任务,也能处理队列中的任务。
  • SHUTDOWN:不接收新任务,但继续处理队列中已保存的任务。
  • STOP:不接收新任务,不处理队列中的任务,且打断正在执行的任务。
  • TIDYING:所有任务已终止,工作线程数为 0,即将调用 terminated() 钩子。
  • TERMINATEDterminated() 方法执行完毕后的最终状态。

2.3 shutdown()shutdownNow() 的差异

两者都是「关门不接新任务」,差别在已提交的任务怎么处理

shutdown()shutdownNow()
新任务❌ 拒绝❌ 拒绝
队列里等待的任务✅ 继续执行❌ 清出队列,作为 List<Runnable> 返回
正在执行的任务✅ 默认跑完(不 interrupt)⚠️ 对 worker 线程 interrupt()
进入状态SHUTDOWNSTOP
气质优雅关停紧急刹车

生活比喻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 后提示/重试/降级
图片压缩池(备选)同上,提交路径保证全在后台同上同上CallerRunsPolicycreateImageCompressPoolWithCallerRuns();主线程提交会 ANR
IO 池DB、文件、下载后处理≈核数*2ArrayBlockingQueue(64)AbortPolicy失败要可见,上层做重试/降级
串行日志池日志、埋点落盘1 / 1ArrayBlockingQueue(200)DiscardPolicy保顺序;高峰可丢低价值日志
实时预览池缩略图、列表项预览2 / 4ArrayBlockingQueue(4)DiscardOldestPolicy旧任务可丢,新任务优先

划分原则:

  1. CPU 密集型(图片解码、JSON 序列化):线程数接近核数,必须有界队列
  2. IO 密集型(网络、磁盘):线程可略多,但仍要上限 + 拒绝策略。
  3. 主线程防护:默认用 AbortPolicy;仅当提交路径全在后台时,才考虑 createImageCompressPoolWithCallerRuns()

4. 实验源码与运行验证

关键源码

文件说明
ThreadPoolBasics.java7 大参数扩容流程、CallerRunsPolicy 演示
AndroidThreadPoolPresets.javaExecutors 工厂陷阱对比 + 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. 对应知识库文档