线程池参数到底要怎么配?

x33g5p2x  于2022-03-07 转载在 其他  
字(4.4k)|赞(0)|评价(0)|浏览(256)

想必大家对Java里面线程池( 类)一定不陌生吧,无论是在日常工作还是面试题里都经常会有它的身影,特别是在当前CPU动辄就是好多核的背景下,了解并使用线程池已经成为一名合格后端开发的基本功了。

相信大家也一定思考过一个问题,面对各种各样的场景,线程池的参数到底应该怎么设计呢?这一定是一个超级难以回答的问题,几天前的我也想不到一个标准的答案,好在是发现了美团在2020年发表过的一篇文章,里面给了一个非常高级的操作——让线程池的参数动态化,这就极大地提高了系统的自适应能力。

https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

至于为什么我现在才看到=_=,可能因为是太懒了吧。。。好在及时发现,在此基础上进行一些分析,不理解线程池的小伙伴们也不用担心,我们首先来回顾一下它的核心思想,在此技术上介绍如何将参数动态化起来~

1 线程池快速回顾

《Java 并发编程的艺术》中提到了使用线程池的好处,概括起来如下:

  • 降低资源损耗。通过重复利用已创建的线程降低线程创建和销毁的损耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。使用线程池可以进行统一的分配,调优和监控。

Java里使用线程池,主要就是用的ThreadPoolExecutor类,先来看一下 ThreadPoolExecutor 类中的构造方法:

/**
 * 用给定的初始参数创建一个新的ThreadPoolExecutor。
 */
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.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

ThreadPoolExecutor 中最重要的参数:

  • corePoolSize:核心线程数。最小可以同时运行的线程数。
  • maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的最大线程数。
  • workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到corePoolSize,如果达到的话,新任务就会被存放在队列中。如果workQueue已经满了的话就执行拒绝策略。

ThreadPoolExecutor 的其他参数:

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被销毁。
  • unit : keepAliveTime 参数的时间单位。
  • threadFactoryexecutor 创建新线程的时候会用到。
  • handler:拒绝策略。

当参数设置完毕后,线程池的工作原理具体是什么呢?我们可以通过下面这个面试题来理解一下:

假设我们设置的线程池参数为:corePoolSize=10, maximumPoolSize=20,queueSize = 10
20个并发任务过来,有多少个活跃线程?

10个。corePoolSize打满,queueSize 也满

21个并发任务过来,有多少个活跃线程?

11个。corePoolSize打满,queueSize 也满还多一个,maximumPoolSize = 20,所以corePoolSize + 1此时活跃的为11个。

30个并发任务过来,有多少个活跃线程?

20个。corePoolSize打满,queueSize 也满,corePoolSize扩充至20,此时有20个活跃任务。

31个并发任务过来,有多少个活跃线程?

20个。corePoolSize打满,queueSize 也满,corePoolSize扩充至20还多一个,如果是丢弃策略,此时有20个活跃任务。

上面的流程可以总结成如下所示的流程图:(来源于https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)

2 现有设置参数的方法及不足

回顾完线程池的核心技术点之后就要开始思考本文主要讨论的内容了:线程池参数应该如何设置?

如果你把这个问题输入到浏览器里,极大可能是下面这种答案:

上面的理论看似很华丽,但现实却是很残酷的。。。你会发现虽然按照上面的指导思想进行配置了,但效果并不能让人满意,造成这种后果的原因有很多,包括但不仅限于:

  1. 任务到底是CPU还是IO密集的特征不明显
  2. 同一个机器上可能部署不止一个服务,不同服务之间也会抢占资源

针对上述问题,美团给出的对应的解决方案就是——线程池参数动态化

那么如何实现参数动态化呢?

接触过微服务开发的同学们可能就会想到了,我们完全可以借助一个配置中心来做,这样就能够实现线程池参数的动态配置和即时生效(在阿里内部也有一个专门的中间件,diamond),省去了重新部署程序并发布的步骤,通常在企业里这一系列流程下来还是比较费时间的。

(来源于https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)

3 如何设置核心线程数(corePoolSize)

其实 ThreadPoolExecutor 类库里直接就有这个方法:

public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
        // We don't really know how many new threads are "needed".
        // As a heuristic, prestart enough new workers (up to new
        // core size) to handle the current number of tasks in
        // queue, but stop if queue becomes empty while doing so.
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}

我们直接看英文注释,这就是作者直接想要表达的意思。大致翻译一下:

设置线程的核心数量,如果新的corePoolSize值小于当前corePoolSize值,多出来的线程将在其下次空闲时被终止。如果新的corePoolSize值大于当前corePoolSize值,就可以创建新的worker来执行队列里的任务

(来源于https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)

4 如何设置最大线程数(maxPoolSize)

同样地 ThreadPoolExecutor 类库里也有这个方法:

public void setMaximumPoolSize(int maximumPoolSize) {
    if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)
        throw new IllegalArgumentException();
    this.maximumPoolSize = maximumPoolSize;
    if (workerCountOf(ctl.get()) > maximumPoolSize)
        interruptIdleWorkers();
}

这个方法的注释和上面的方法类似,大家可以对照着看:

逻辑也并不复杂:

  1. 参数校验
  2. 设置最大线程数 maxPoolSize
  3. 如果工作线程数是否大于最大线程数,则对空闲线程发起中断

JDK原生线程池ThreadPoolExecutor还提供了其他设置参数的方法:

5 如何改变等待队列长度

等待队列的长度capacityfinal修饰符修饰,所以按理说是不能修改的

private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

唯一可能的办法就是自己定义一个队列,在美团的实现里就是一个名为ResizableCapacityLinkedBlockIngQueue的队列,根据名称也不难看出,这个队列的容量是可变的。

具体的实现细节美团好像并没有公布出来,不过我们可以简单的将原先LinkedBlockingQueuecapacityfinal修饰符去掉,并提供getter和setter方法,形成我们自己的ResizableCapacityLinkedBlockIngQueue

相关文章