线程池参数知多少

207 阅读7分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

线程池的参数

线程池的参数直接看源码


public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

关于参数的说明其实,官方的注释写的都非常明白了。看代码的时候一定要结合注释,因为英文是 Doug Lea(作者)写的,表达的是作者的准确的想法。

一、corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set

(核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut。)

二、maximumPoolSize:the maximum number of threads to allow in the pool。

(最大线程数:线程池中最多允许创建 maximumPoolSize 个线程。)

三、keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating。

(存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。)

四、unit:the time unit for the {@code keepAliveTime} argument

(keepAliveTime 的时间单位。)

五、workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method。

(存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。所以这里就不要翻译为工作队列了,好吗?不要自己给自己挖坑。)

六、threadFactory:the factory to use when the executor creates a new thread。

(线程工程:用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的,不会懵逼。)

七、handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached。

(拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。系统默认的拒绝策略有以下几种:

  1. AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。
  2. DiscardPolicy:直接抛弃不处理。
  3. DiscardOldestPolicy:丢弃队列中最老的任务。
  4. CallerRunsPolicy:将任务分配给当前执行execute方法线程来处理。

我们还可以自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可,友好的拒绝策略实现有如下:

  1. 将数据保存到数据,待系统空闲时再进行处理。
  2. 将数据用日志进行记录,后由人工处理。)

上面的 7 个参数中,我们主要需要关心的参数是:corePoolSize、maximumPoolSize、workQueue(队列长度)。因为自定义线程池需要考虑如何设置这几个参数

线程池参数如何设置

线程池使用面临的核心的问题在于:线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大。

现在提个问题:现有一个线程池,参数corePoolSize = 8,maximumPoolSize = 16,BlockingQueue阻塞队列长度为8,有7个任务同时进来,问:线程池会创建几条线程?

如果7个任务还没处理完,这时又同时进来2个任务,问:线程池又会创建几条线程还是不会创建?

如果前面9个任务还是没有处理完,这时又同时进来8个任务,问:线程池又会创建几条线程还是不会创建?

其实这就是在问你线程池的执行流程了,简单的说一下就是:线程池corePoolSize=8,线程初始化时不会自动创建线程,所以当有7个任务同时进来时,执行execute方法会新建7条线程来执行任务;

前面的7个任务都没完成,现在又进来2个队列,会新建1条线程来执行任务,这时poolSize=corePoolSize,还剩下1个任务,线程池会将剩下这个任务塞进阻塞队列中,等待空闲线程执行;

如果前面9个任务还是没有处理完,这时又同时进来了8个任务,此时还没有空闲线程来执行新来的任务,所以线程池继续将这8个任务塞进阻塞队列,但发现阻塞队列已经满了,核心线程也用完了,还剩下1个任务不知道如何是好,于是线程池只能创建1条“临时”线程来执行这个任务了;

这里创建的线程用“临时”来描述还是因为它们不会长期存在于线程池,它们的存活时间为keepAliveTime,此后线程池会维持最少corePoolSize数量的线程。

这样是不是更容易理解了,再来看一个例子:一个线程池,参数corePoolSize = 10,maximumPoolSize = 30,BlockingQueue阻塞队列长度为100

问:如果这个线程池接受到了 30 个比较耗时的任务,这个时候线程池的状态(或者说数据)是怎样的?

在前面 30 个比较耗时的任务还没执行完成的情况下,再来多少个任务会触发拒绝策略?

直接说答案了:当接收到了 30 个比较耗时的任务时,10 个核心线程数都在工作,剩下的 20 个去队列里面排队。这个时候和最大线程数是没有关系的,所以和线程存活时间也就没有关系。

其实如果知道这个线程池最多能接受多少任务,就知道这个题的答案是什么了,上面的线程池中最多接受 100(队列长度) + 30(最大线程数) = 130 个任务。所以当已经接收了30个任务的情况下,如果再来 100 个比较耗时的任务,这个时候队列也满了,最大线程数的线程也都在工作,这个时候线程池满载了。因此,在前面 30 个比较耗时的任务还没执行完成的情况下,再来 101 个任务,第 101 个任务就会触发线程池的拒绝策略了。

实际场景参数设置

现在大多数的答案都是先区分线程池中的任务是 IO 密集型还是 CPU 密集型。如果是 CPU 密集型的,可以把核心线程数设置为核心数+1。《Java并发编程实战》一书中给出的解释是:即使当计算(CPU)密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保 CPU 的时钟周期不会被浪费。可以理解为一个备份吧。

此外还有个需要注意的小点就是,如果你的服务器上部署的不止一个应用,你就得考虑其他的应用的线程池配置情况。 经过精密的计算,你咔一下设置好参数,结果项目部署上去了,发现还有其他的应用在和你抢 CPU,多难受。

《Java并发编程实战》一书中给出的计算方式是这样的:

640.png

这个东西面试的时候有用。真实场景中只能得到一个参考值,基于这个参考值,再去进行调整。