ThreadPoolExecutor工作流程记录

ThreadPoolExecutor工作流程记录

在一些频繁使用子线程的场景下,为了减少线程的创建和销毁带来的开销,通常使用ThreadPoolExecutor(线程池)来执行多线程任务。本文主要分析手动创建线程池用到的几个参数及其含义,以及分析一个任务被提交到线程池后该任务会被谁消费掉。

参数含义

线程池ThreadPoolExecutor的构造函数包含以下7个参数:

参数名含义
int corePoolSize核心线程数,如果提交任务时已正在运行的线程数量小于此值,则直接新建线程并执行任务。
int maximumPoolSize最大线程数,当已提交的任务数量大于corePoolSize时会尝试将任务添加到workQueue里,如果workQueue添加失败,会判断当前线程数量是否小于maximumPoolSize,如果小于则创建线程并执行。
long keepAliveTime非核心线程存活的时间,超过这个时间除了核心线程外的非核心线程会被销毁,例如corePoolSize为10,maximumPoolSize为15,假设当前创建线程的数量是13,超过存活时间后只剩下10个核心线程,另外3个会被销毁。
TimeUnit unitkeepAliveTime的单位,可以设置为纳秒、微秒、毫秒、秒、分、小时、天。
BlockingQueue workQueue一个用来存放Runnable的阻塞队列,当任务数量大于核心线程数时就会尝试将任务添加到workQueue。
ThreadFactory threadFactory线程工厂,提供创建自定义Thread的方法,方便在创建Thread时添加自定义线程名称、插入相关逻辑等,便于调试和追溯问题所在。
RejectedExecutionHandler handler任务被拒绝执行的处理程序,提交新任务时如果workQueue已满且,线程数量等于maximumPoolSize时,则会调用handler的rejectedExecution方法,可以在rejectedExecution方法中处理相关逻辑。
工作流程

调用ThreadPoolExecutor的execute方法将任务添加到线程池时,会执行以下步骤:

  1. 判断当前线程数是否小于corePoolSize,如果小于则调用threadFactorynewThread方法创建一个新的子线程并且执行任务。否则执行步骤2。
  2. 尝试将任务添加到workQueue中,添加成功则等待核心线程执行任务,如果workQueue已满则添加失败,执行步骤3。
  3. 判断当前线程数量如果小于maximumPoolSize则创建并启动子线程执行该任务,否则执行步骤4。
  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-1Thread-2Thread-3Thread-4Thread-5已被销毁,为了执行新提交的任务又创建了三个子线程,至此我们也应该感受到了 corePoolSizemaximumPoolSizeworkQueuekeepAliveTime 的作用。

总结
  1. 提交任务时,如果 当前线程数 < corePoolSize ,则创建核心线程并执行该任务。
  2. 提交任务时,如果 当前线程数 ≥ corePoolSize,且workQueue容量没满,则将该任务添加到workQueue中,等待被执行。
  3. 提交任务时,如果workQueue容量已满,且 corePoolSize ≤ 当前线程数 < maximumPoolSize,则创建非核心线程执行该任务。
  4. 提交任务时,如果workQueue容量已满,当前线程数 ≥ maximumPoolSize,则将该任务交给拒绝策略处理。
  5. 如果使用过阿里巴巴Java规约插件的朋友应该知道,阿里不允许直接使用Executors创建线程池,我了解的原因主要是因为Executors创建出来的线程池有的maximumPoolSizeworkQueue容量Integer.MAX_VALUE,容易积压过多任务或创建过多线程导致OOM,因此提倡手动创建线程池,根据使用场景配置相关参数。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值