ThreadPoolExecutor工作流程记录
在一些频繁使用子线程的场景下,为了减少线程的创建和销毁带来的开销,通常使用ThreadPoolExecutor(线程池)来执行多线程任务。本文主要分析手动创建线程池用到的几个参数及其含义,以及分析一个任务被提交到线程池后该任务会被谁消费掉。
参数含义
线程池ThreadPoolExecutor的构造函数包含以下7个参数:
参数名 | 含义 |
---|---|
int corePoolSize | 核心线程数,如果提交任务时已正在运行的线程数量小于此值,则直接新建线程并执行任务。 |
int maximumPoolSize | 最大线程数,当已提交的任务数量大于corePoolSize 时会尝试将任务添加到workQueue里,如果workQueue添加失败,会判断当前线程数量是否小于maximumPoolSize ,如果小于则创建线程并执行。 |
long keepAliveTime | 非核心线程存活的时间,超过这个时间除了核心线程外的非核心线程会被销毁,例如corePoolSize 为10,maximumPoolSize 为15,假设当前创建线程的数量是13,超过存活时间后只剩下10个核心线程,另外3个会被销毁。 |
TimeUnit unit | keepAliveTime 的单位,可以设置为纳秒、微秒、毫秒、秒、分、小时、天。 |
BlockingQueue workQueue | 一个用来存放Runnable的阻塞队列,当任务数量大于核心线程数时就会尝试将任务添加到workQueue。 |
ThreadFactory threadFactory | 线程工厂,提供创建自定义Thread的方法,方便在创建Thread时添加自定义线程名称、插入相关逻辑等,便于调试和追溯问题所在。 |
RejectedExecutionHandler handler | 任务被拒绝执行的处理程序,提交新任务时如果workQueue已满且,线程数量等于maximumPoolSize 时,则会调用handler 的rejectedExecution方法,可以在rejectedExecution方法中处理相关逻辑。 |
工作流程
调用ThreadPoolExecutor的execute
方法将任务添加到线程池时,会执行以下步骤:
- 判断当前线程数是否小于
corePoolSize
,如果小于则调用threadFactory
的newThread
方法创建一个新的子线程并且执行任务。否则执行步骤2。 - 尝试将任务添加到workQueue中,添加成功则等待核心线程执行任务,如果workQueue已满则添加失败,执行步骤3。
- 判断当前线程数量如果小于
maximumPoolSize
则创建并启动子线程执行该任务,否则执行步骤4。 - 如果提交的任务经过上面几个步骤都没被消费掉,则会被调度到
handler
(拒绝策略)的rejectedExecution
方法,我们可以自定义拒绝策略处理被拒绝的任务。
官方提供四种常用的拒绝策略
策略名 介绍 AbortPolicy 默认的拒绝策略,当有任务被拒绝后会抛出RejectedExecutionException错误。 DiscardPolicy 啥都不干,直接丢弃任务被拒绝的任务。 DiscardOldestPolicy 将workQueue中最老的任务丢弃掉,并将被拒绝的任务添加到workQueue中。 CallerRunsPolicy 判断线程池是否已关闭,如果没关闭则在创建线程池的线程中直接运行被拒绝的任务。
示例和分析
为了更直观的看到观察到任务提交后线程池会发生什么变化、执行哪些操作,我写了下面这段代码,我们可以根据所打印的信息来分析这几种情况:
- 核心线程创建的时机
- 非核心线程创建的时机
- 什么情况下任务会被添加到workQueue
- 什么情况下任务被拒
/**
* 自定义Runnable类,run方法中睡眠1秒,方便观察线程池的创建子线程
*/
class MyRun(private val number: Int, val name: String) : Runnable {
override fun run() {
Thread.sleep(1000L)
println("执行任务: MyRun $number -- ${Thread.currentThread().name}")
}
}
fun main(args: Array<String>) {
// 记录累计创建线程的数量,便于观察
val count = AtomicInteger(1)
// 创建线程池
val executor = ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数,不能小于核心线程数,否则报错
2L, // 存活时间2秒
TimeUnit.SECONDS, // 存活时间单位:秒
ArrayBlockingQueue(3), // 阻塞队列的容量为3
// 自定义ThreadFactory,创建线程时自定义线程名称
ThreadFactory {
val threadName = "Thread-${count.getAndIncrement()}"
printGreen("创建线程: $threadName")
Thread(it, threadName)
},
// 自定义拒绝策略,打印出被拒绝的任务名
RejectedExecutionHandler { runnable, _ ->
if (runnable is MyRun) {
printBrown("丢弃任务: ${runnable.name}")
}
}
)
// 第一波提交10个任务
repeat(10) {
val number = it.inc()
printRed("提交任务: MyRun $number")
executor.execute(MyRun(number, "MyRun $number"))
}
// 等待4秒,让非核心线程被销毁掉
Thread.sleep(5000L)
// 第二波提交10个任务,用来观察空非核心线程是否被销毁
repeat(10) {
val number = it.inc() + 10
printRed("提交任务: MyRun $number")
executor.execute(MyRun(number, "MyRun $number"))
}
}
第一波提交任务后的打印日志
第二波提交任务后的打印日志
从第二次提交完任务后打印的日志可以看出存活下来的核心线程为Thread-1
和 Thread-2
,Thread-3
、Thread-4
、Thread-5
已被销毁,为了执行新提交的任务又创建了三个子线程,至此我们也应该感受到了 corePoolSize
、 maximumPoolSize
、workQueue
和 keepAliveTime
的作用。
总结
- 提交任务时,如果 当前线程数 <
corePoolSize
,则创建核心线程并执行该任务。 - 提交任务时,如果 当前线程数 ≥
corePoolSize
,且workQueue容量没满,则将该任务添加到workQueue中,等待被执行。 - 提交任务时,如果workQueue容量已满,且
corePoolSize
≤ 当前线程数 <maximumPoolSize
,则创建非核心线程执行该任务。 - 提交任务时,如果workQueue容量已满,当前线程数 ≥
maximumPoolSize
,则将该任务交给拒绝策略处理。 - 如果使用过阿里巴巴Java规约插件的朋友应该知道,阿里不允许直接使用Executors创建线程池,我了解的原因主要是因为Executors创建出来的线程池有的
maximumPoolSize
或workQueue容量
为Integer.MAX_VALUE
,容易积压过多任务或创建过多线程导致OOM,因此提倡手动创建线程池,根据使用场景配置相关参数。