java线程池技术

Java线程池实现原理及其在美团业务中的实践

 

随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一个开发人员必修的基本功。

本文开篇简述线程池概念和用途,接着结合线程池的源码,帮助读者领略线程池的设计思路,最后回归实践,通过案例讲述使用线程池遇到的问题,并给出了一种动态化线程池解决方案。

一、写在前面

1.1 线程池是什么

线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。

线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

而本文描述线程池是JDK中提供的ThreadPoolExecutor类。

当然,使用线程池可以带来一系列好处:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。

  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。

  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

1.2 线程池解决的问题是什么

线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。

  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。

  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

Pooling is the grouping together of resources (assets, equipment, personnel, effort, etc.) for the purposes of maximizing advantage or minimizing risk to the users. The term is used in finance, computing and equipment management.——wikipedia

“池化”思想不仅仅能应用在计算机领域,在金融、设备、人员管理、工作管理等领域也有相关的应用。

在计算机领域中的表现为:统一管理IT资源,包括服务器、存储、和网络资源等等。通过共享资源,使用户在低投入中获益。除去线程池,还有其他比较典型的几种使用策略包括:

  1. 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。

  2. 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。

  3. 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。

在了解完“是什么”和“为什么”之后,下面我们来一起深入一下线程池的内部实现原理。

二、线程池核心设计与实现

在前文中,我们了解到:线程池是一种通过“池化”思想,帮助我们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类。那么它的的详细设计与实现是什么样的呢?我们会在本章进行详细介绍。

2.1 总体设计

Java中的线程池核心实现类是ThreadPoolExecutor,本章基于JDK 1.8的源码来分析Java线程池的核心设计与实现。我们首先来看一下ThreadPoolExecutor的UML类图,了解下ThreadPoolExecutor的继承关系。

图1 ThreadPoolExecutor UML类图

ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。

AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?其运行机制如下图所示:

图2 ThreadPoolExecutor运行流程

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,

当任务提交后,线程池会判断该任务后续的流转
(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

接下来,我们会按照以下三个部分去详细讲解线程池运行机制:

  1. 线程池如何维护自身状态。

  2. 线程池如何管理任务。

  3. 线程池如何管理线程。

2.2 生命周期管理

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起,如下代码所示:hljs">private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

ctl这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。

关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如以下代码所示:

  1. private static  int runStateOf( int c)     {  return c & ~CAPACITY; }  //计算当前运行状态
  2. private static  int workerCountOf( int c)  {  return c & CAPACITY; }   //计算当前线程数量
  3. private static  int ctlOf( int rs,  int wc) {  return rs | wc; }    //通过状态和线程数生成ctl

ThreadPoolExecutor的运行状态有5种,分别为:

其生命周期转换如下入所示:

图3 线程池生命周期

2.3 任务执行机制

2.3.1 任务调度

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。

首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

其执行流程如下图所示:

图4 任务调度流程

2.3.2 任务缓冲

任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

下图中展示了线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素:

图5 阻塞队列

使用不同的队列可以实现不一样的任务存取策略。在这里,我们可以再介绍下阻塞队列的成员:

2.3.3 任务申请

由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:

图6 获取任务流程图

getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。

2.3.4 任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略是一个接口,其设计如下:

  1. public  interface RejectedExecutionHandler {
  2.     void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
  3. }

用户可以通过实现这个接口去定制拒绝策略,也可以选择JDK提供的四种已有拒绝策略,其特点如下:

2.4 Worker线程管理

2.4.1 Worker线程

线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker。我们来看一下它的部分代码:

  1. private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
  2.     final Thread thread; //Worker持有的线程
  3.     Runnable firstTask; //初始化的任务,可以为null
  4. }

Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

Worker执行任务的模型如下图所示:

图7 Worker执行任务

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。

Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。

  1. lock方法一旦获取了独占锁,表示当前线程正在执行任务中。

  2. 如果正在执行任务,则不应该中断线程。

  3. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。

  4. 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

在线程回收过程中就使用到了这种特性,回收过程如下图所示:

图8 线程池回收过程

2.4.2 Worker线程增加

增加线程是通过线程池中的addWorker方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示:

图9 申请线程执行流程图

2.4.3 Worker线程回收

线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

  1. try {
  2.   while (task != null || (task = getTask()) != null) {
  3.      //执行任务
  4.   }
  5. } finally {
  6.   processWorkerExit(w, completedAbruptly); //获取不到任务时,主动回收自己
  7. }

线程回收的工作是在processWorkerExit方法完成的。

图10 线程销毁流程

事实上,在这个方法中,将线程引用移出线程池就已经结束了线程销毁的部分。但由于引起线程销毁的可能性有很多,线程池还要判断是什么引发了这次销毁,是否要改变线程池的现阶段状态,是否要根据新状态,重新分配线程。

2.4.4 Worker线程执行任务

在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:

  1. while循环不断地通过getTask()方法获取任务。

  2. getTask()方法从阻塞队列中取任务。

  3. 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。

  4. 执行任务。

  5. 如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。

执行流程如下图所示:

图11 执行任务流程

了解了线程池基本属性的概念是远远不够的,还需要知道每一个属性在源码中的体现,比如提交任务的过程中是如何将核心线程数、工作队列、最大线程数以及拒绝策略等连起来的?工作线程是如何执行任务代码的?线程池是如何回收空闲线程的?

默认情况下,刚初始化好的线程池是没有任何存活的线程的,等到有任务提交才创建线程执行任务。如果实际使用中希望线程池初始时尽快执行任务,可以调用prestartAllCoreThreads或者prestartCoreThread方法,预先在线程池启动几个工作线程,等待任务提交并执行。

//循环启动corePoolSize个工作线程
public int prestartAllCoreThreads() {
    int n = 0;
    while (addWorker(null, true))
        ++n;
    return n;
}
//在corePoolSize范围内启动一个工作线程
//个人认为无需在外面判断workerCountOf(ctl.get()) < corePoolSize
//因为addWorker里面会判断线程数是否满足corePoolSize
public boolean prestartCoreThread() {
    return workerCountOf(ctl.get()) < corePoolSize &&
        addWorker(null, true);
}

 
 

execute()提交任务

线程池是一个生产者消费者模式,execute()提交任务是生产者,工作线程从工作队列拉取任务执行是消费者。
execute()代码逻辑主要分为4步:

  1. 如果工作线程数小于corePoolSize,则直接启动一个新的工作线程。
  2. 如果工作线程数大于等于corePoolSize,则将任务加入workQueue阻塞队列。
  3. 阻塞队列队列装满且正在运行的线程数小于maximumPoolSize,则创建一个新的工作线程。
  4. 当正在运行的线程数大于等于maximumPoolSize,将调用拒绝策略。
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        //1、worker数小于corePoolSize,直接创建worker线程
        if (addWorker(command, true))
            //创建成功,返回
            return;
        c = ctl.get();
    }
    //2、线程数大于等于corePoolSize,任务加入workQueue阻塞队列
    //2.1.判断线程池状态是否为running,是并加入阻塞队列
    if (isRunning(c) && workQueue.offer(command)) {
        //2.2.再次检查线程池状态是否running
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            //2.2.1 线程池不在running态,将任务从阻塞队列删除,调用拒绝策略
            reject(command);
        //若线程数为0则启动一个空线程从workQueue中取任务
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //不是running或者任务加入阻塞队列失败(阻塞队列满了)
    //3、判断maximumPoolSize,阻塞队列已满且小于maximumPoolSize,创建新的worker
    else if (!addWorker(command, false))
        //4、大于等于maximumPoolSize,拒绝策略
        reject(command);
}

 
 

    当线程数大于等于corePoolSize,任务被加入workQueue时,对线程池的状态进行了double check,这是有必要的:

    1. 在将任务加入workQueue前判断线程池是否是RUNNING,不是则不加入workQueue
    2. 任务加入workQueue后还需要再判断一次线程池的状态是否是RUNNING,因为在这个过程中可能线程池死亡或者外部调用了shundown() 或者 shutdownNow()使得线程池正在销毁,此时需要删除刚加入工作队列的任务,并触发拒绝策略。

    当任务加入workQueue后判断此时工作线程数为0,则启动一个空工作线程消费workQueue中的任务。

    addWorker() 创建并启动工作线程

    execute()的核心在addWorker(Runnable firstTask, boolean core),它是如何创建并启动一个工作线程的呢?addWorker第二个参数为true时线程数与corePoolSize比较,false时线程数与maximumPoolSize比较。

    addWorker代码很长,有两个大的for循环,逻辑主要涉及2步:

    • 第一个for循环:自旋CAS操作workerCount加1。
    • 第二个for循环:创建并启动新Worker线程。

    workerCount加1和创建worker时都反复判断线程池当前的运行状态,是为了当线程池调用了shutdown()或者shutdownNow()时做出相应的措施。

    
    private boolean addWorker(Runnable firstTask, boolean core) {
        //1、这个循环主要是为了自旋 cas  workerCount+1,成功之后就创建worder
        // 因为代码块没有加锁,且是有可能多个线程同时操作,所以采用了cas乐观锁的方式。
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            // Check if queue empty only if necessary.
            //线程池状态 大于等于SHUTDOWN(不是running),且非(SHUTDOWN 且task为null且阻塞队列不为空)
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;
            for (;;) {
                int wc = workerCountOf(c);
                //worker数>=CAPACITY 或者worker数>=corePoolSize or maximumPoolSize 直接返回false
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //cas workercount+1
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
        //2、创建worker
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //1、new Worker
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                //1.1、这里需要加锁
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    //当获取锁后再次检查 线程池状态
                    int rs = runStateOf(ctl.get());
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        //工作线程已经是startable
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //2、工作线程还未启动,加入workers(HashSet)
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            //2.2、实时修改largestPoolSize = s
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //启动工作线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                //worder线程启动失败后的一些措施
                //1、从workers删除worder
                //2、worderCount递减
                //3、尝试终止线程池
                addWorkerFailed(w);
        }
        return workerStarted;
    }
    

    第一个for循环,workerCount自旋CAS加1的过程中判断线程数是否满足corePoolSize或者maximumPoolSizeworkerCount加1成功结束外围循环,也有可能在CAS的过程中workerCount改变导致失败则重新判断、自旋重试。

    第二个for循环,创建Worker工作线程,并启动,启动的过程中需要加锁,因为启动完Worker,还需要将其加入到workers中,并将workers.size()赋值给largestPoolSize;如果Worker启动失败,则需要一些措施:

    1. workers删除Worder
    2. worderCount自旋CAS递减。
    3. 如果线程池正处于销毁的过程,则尝试自旋终止线程池。
    private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (w != null)
                //1、从workers删除worder
                workers.remove(w);
            //2、worderCount cas自旋递减
            decrementWorkerCount();
            //3、如果线程池正处于销毁的过程,则尝试自旋终止线程池
    

    Worker类是一把锁也是一个线程

    addWorker中创建的工作线程是Worker对象,它是ThreadPoolExecutor的内部类,继承于AbstractQueuedSynchronizer,相当于是一把锁,实现了Runnable接口,又是一个线程。

    Worker有两个核心的成员变量threadfirstTask。因为Worker本身是一个Runnable线程,初始化时会被ThreadFactory包装创建为一个Thread赋值给threadaddWorker方法中直接调用thread.start()启动Worker线程;firstTask是提交进来的任务,Worker直接调用firstTask的run()函数,执行任务代码。

    public void run() {
        runWorker(this);
    }
    final void runWorker(Worker w) {
            Thread wt = Thread.currentThread();
            Runnable task = w.firstTask;
            w.firstTask = null;
            w.unlock(); // allow interrupts
            boolean completedAbruptly = true;
            try {
                while (task != null || (task = getTask()) != null) {
                    w.lock();
                    if ((runStateAtLeast(ctl.get(), STOP) ||
                         (Thread.interrupted() &&
                          runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted())
                        wt.interrupt();
                    try {
                        //这里是一个空函数,使用者可以自行继承ThreadPoolExecutor 并重写beforeExecute
                        beforeExecute(wt, task);
                        Throwable thrown = null;
                        try {
                            //直接调用了run函数
                            task.run();
                        } catch (RuntimeException x) {
                            thrown = x; throw x;
                        } catch (Error x) {
                            thrown = x; throw x;
                        } catch (Throwable x) {
                            thrown = x; throw new Error(x);
                        } finally {
                            //这也是一个空函数,使用者可以自行继承ThreadPoolExecutor 并重写afterExecute
                            afterExecute(task, thrown);
                        }
                    } finally {
                        task = null;
                        w.completedTasks++;
                        w.unlock();
                    }
                }
                completedAbruptly = false;
            } finally {
                //如果从阻塞队列取不到任务,将会让当前的工作线程退出
                processWorkerExit(w, completedAbruptly);
            }
    }
    

    runWorker执行任务

    Worker线程启动后会自动条用其run函数:

    Worker刚start时,直接拿Worker的成员变量firstTask来执行任务代码,当firstTask为空时,从工作队列中循环取出task执行,而工作队列为空则阻塞等待。

    runWorker()在代码中提供了两个空函数beforeExecuteafterExecute,一般称之为钩子函数,用户可以自行继承ThreadPoolExecutor时,将其重写,加上一些业务逻辑。

    public void run() {
        runWorker(this);
    }
    final void runWorker(Worker w) {
            Thread wt = Thread.currentThread();
            Runnable task = w.firstTask;
            w.firstTask = null;
            w.unlock(); // allow interrupts
            boolean completedAbruptly = true;
            try {
                while (task != null || (task = getTask()) != null) {
                    w.lock();
                    if ((runStateAtLeast(ctl.get(), STOP) ||
                         (Thread.interrupted() &&
                          runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted())
                        wt.interrupt();
                    try {
                        //这里是一个空函数,使用者可以自行继承ThreadPoolExecutor 并重写beforeExecute
                        beforeExecute(wt, task);
                        Throwable thrown = null;
                        try {
                            //直接调用了run函数
                            task.run();
                        } catch (RuntimeException x) {
                            thrown = x; throw x;
                        } catch (Error x) {
                            thrown = x; throw x;
                        } catch (Throwable x) {
                            thrown = x; throw new Error(x);
                        } finally {
                            //这也是一个空函数,使用者可以自行继承ThreadPoolExecutor 并重写afterExecute
                            afterExecute(task, thrown);
                        }
                    } finally {
                        task = null;
                        w.completedTasks++;
                        w.unlock();
                    }
                }
                completedAbruptly = false;
            } finally {
                //如果从阻塞队列取不到任务,将会让当前的工作线程退出
                processWorkerExit(w, completedAbruptly);
            }
    }
    
     
     

      runWorker()源码中显而易见,直接调用的是Runnable任务taskrun,并且try-catch捕获了异常,却只是上抛并没有处理。如果提交的Runnable任务run方法中有异常,用户没有自行捕获,Worker捕获了,则会结束循环,并会将当前worker线程销毁,影响线程池的正常使用,所以强制要求Runnable任务run方法中任务代码try-catch,防患于未然。

      Worker是一把锁,在runWorker()循环执行任务时会上锁,其目的是确保Worker线程,除了线程池销毁导致中断外,没有其他中断的设置。在调用shutdown()关闭线程池时,会中断空闲的工作线程,就是通过遍历workers,判断是否能获取Worker线程的锁,而定义Worker线程是否为空闲。源码英文注释是这样描述的:

      Before running any task, the lock is acquired to prevent other pool interrupts

      while the task is executing, and then we ensure that unless pool is stopping,

      this thread does not have its interrupt set.

      getTask()决定Worker的生死命运

      getTask()是从workerQueue中获取任务,涉及了如何从队列中取任务,以及决定了工作线程Worker的生死命运。

      getTask()返回null是一件非常恐怖的事情,因为runWorker会因为getTask()返回null而结束循环,从而执行销毁退出Worker线程的逻辑,也就是回收Worker线程。getTest()返回null的情况:

      1. getTest()本身是个for循环,会不断判断线程池的运行状态,如果线程池正处于STOP状态以上或者处于SHUTDOWN状态且workQueue为空,会将workerCount递减为0,并返回null。
      2. 当工作线程数大于maximumPoolSize且工作线程大于1时,workerCount递减1,并返回null。意图是回收该Worker线程,使线程数保持在maximumPoolSize数量内。
      3. 具有超时效果timed = true,大于corePoolSize的工作线程都具有超时效果,而设置allowCoreThreadTimeOut为true,小于corePoolSize的工作线程也具有超时效果,此时超时则返回null。
      4. 去任务的过程中poll 超时返回null。
      private Runnable getTask() {
          boolean timedOut = false; // Did the last poll() time out?
          for (;;) {
              int c = ctl.
      private Runnable getTask() {
          boolean timedOut = false; // Did the last poll() time out?
          for (;;) {
              int c = ctl.get();
              int rs = runStateOf(c);
              // Check if queue empty only if necessary.
              if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                  decrementWorkerCount();
                  return null;
              }
              int wc = workerCountOf(c);
              // Are workers subject to culling?
              boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
              //大于maximumPoolSize时,将不从阻塞队列中取
              //大于corePoolSize 或者 allowCoreThreadTimeOut 为true,且超时了不从阻塞队列中取
              if ((wc > maximumPoolSize || (timed && timedOut))
                  && (wc > 1 || workQueue.isEmpty())) {
                  if (compareAndDecrementWorkerCount(c))
                      return null;
                  continue;
              }
              try {
                  //通过timed判断是用poll还是take,
                  Runnable r = timed ?
                      workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                      workQueue.take();
                  if (r != null)
                      return r;
                  timedOut = true;
              } catch (InterruptedException retry) {
                  timedOut = false;
              }
          }
      }

      通过timed判断如何从队列中取任务:

      1. timed=true使用具有时间的poll,如果取不到会阻塞一段时间,超时还未取到则返回null。
      2. time=false使用take(),取不到任务会一直阻塞。

      一般情况下allowCoreThreadTimeOut=false,只有超过corePoolSize的线程具有超时效果,受keepAliveTime的支配。如果使用者将allowCoreThreadTimeOut设置为true,则所有的工作线程都会因为空闲超时而被回收。

      processWorkerExit因runWorker循环结束而回收Worker

      processWorkerExitrunWorker循环结束而回收Worker,主要做了如下几件事:

      1. 将该Worker线程的任务完成数统计加给全局completedTaskCount
      2. workers删除worker
      3. 如果线程池处于销毁状态,尝试继续推进销毁状态。
      4. 可能会启动一个空的Worker线程。
      
      private void processWorkerExit(Worker w, boolean completedAbruptly) {
          if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
              decrementWorkerCount();
          final ReentrantLock mainLock = this.mainLock;
          mainLock.lock();
          try {
              //统计完成的任务数
              completedTaskCount += w.completedTasks;
              //从workers删除worker
              workers.remove(w);
          } finally {
              mainLock.unlock();
          }
          //重试终止,如果线程池状态为running 直接返回
          tryTerminate();
          int c = ctl.get();
          if (runStateLessThan(c, STOP)) {
              //stop以内,completedAbruptly=false
              if (!completedAbruptly) {
                  int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                  if (min == 0 && ! workQueue.isEmpty())
                      min = 1;
                  if (workerCountOf(c) >= min)
                      return; // replacement not needed
              }
              //allowCoreThreadTimeOut=true,且没有正在运行的工作线程
              //或者allowCoreThreadTimeOut=false,工作线程数没有达到corePoolSize
              //则创建一个空工作线程作为弥补。
              addWorker(null, false);
          }
      }
      

      runWorker中局部变量completedAbruptly首先设置为true,循环结束则设置completedAbruptly为false,但是若是因为任务代码异常,则不会执行到,所以不会再销毁的当前Worker的时候再启动一个空Worker弥补上。

      什么时候会再启动一个空Worker呢?当线程池的状态小于STOP,也就是RUNNING或者SHUTDOWN,且不是因为任务代码异常结束循环时,继续判断是否需要启动一个空Worker线程作为弥补:

      1. allowCoreThreadTimeOut=true,且没有正在运行的工作线程且workQueue不为空时,启动一个空Worker继续消费workQueue中的任务。
      2. allowCoreThreadTimeOut=false,工作线程数没有达到corePoolSize,则创建一个空工作线程作为弥补。

      总结

      1. 可以调用prestartAllCoreThreads或者prestartCoreThread方法预先创建几个工作线程池,等待任务提交并执行。
      2. 线程池中通过创建并启动Worker线程,执行用户提交的任务。
      3. Worker继承于AbstractQueuedSynchronizer,是一把锁,实现了Runnable接口,又是一个线程。
      4. 用户提交的Runnable任务task,启动Worker后自动执行Workerrun(),并直接调用taskrun()任务代码。
      5. 用户提交的任务,需要自行try-catch,否则一旦出现异常,被Worker捕获,但不处理直接上报,使得Worker线程被回收,影响线程池的正常使用。
      6. 工作线程Worker的回收是在Worker循环从workerQueue获取任务时,超过keepAliveTime依然没有获取到任务,则返回null,导致Worker循环中断,最后执行Worker销毁退出。
      7. 设置allowCoreThreadTimeOut为true,所有的线程都会因为超时而被回收。
      8. Worker是一把锁,在
        runWorker()循环执行任务时会上锁,其目的是确保Worker线程,除了线程池销毁导致中断外,没有其他中断的设置。

      PS: 如若文章中有错误理解,欢迎批评指正,同时非常期待你的评论、点赞和收藏。我是徐同学,愿与你共同进步!

      三、线程池在业务中的实践

      3.1 业务背景

      在当今的互联网业界,为了最大程度利用CPU的多核性能,并行运算的能力是不可或缺的。通过线程池管理线程获取并发性是一个非常基础的操作,让我们来看两个典型的使用线程池获取并发性的场景。

      场景1:快速响应用户请求

      描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。

      分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

      图12 并行执行任务提升任务响应速度

      场景2:快速处理批量任务

      描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。

      分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

      图13 并行执行任务提升批量任务执行速度

      3.2 实际问题及方案思考

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

      关于线程池配置不合理引发的故障,公司内部有较多记录,下面举一些例子:

      Case1:2018年XX页面展示接口大量调用降级。

      事故描述:XX页面展示接口产生大量调用降级,数量级在几十到上百。

      事故原因:该服务展示接口内部逻辑使用线程池做并行计算,由于没有预估好调用的流量,导致最大核心数设置偏小,大量抛出RejectedExecutionException,触发接口降级条件,示意图如下:

      图14 线程数核心设置过小引发RejectExecutionException

      Case2:2018年XX业务服务不可用S2级故障。

      事故描述:XX业务提供的服务执行时间过长,作为上游服务整体超时,大量下游服务调用失败。

      事故原因:该服务处理请求内部逻辑使用线程池做资源隔离,由于队列设置过长,最大线程数设置失效,导致请求数量增加时,大量任务堆积在队列中,任务执行时间过长,最终导致下游服务的大量调用超时失败。示意图如下:

      图15 线程池队列长度设置过长、corePoolSize设置过小导致任务执行速度低

      业务中要使用线程池,而使用不当又会导致故障,那么我们怎样才能更好地使用线程池呢?针对这个问题,我们下面延展几个方向:

      1. 能否不用线程池?

      回到最初的问题,业务使用线程池是为了获取并发性,对于获取并发性,是否可以有什么其他的方案呢替代?我们尝试进行了一些其他方案的调研:

      综合考虑,这些新的方案都能在某种情况下提升并行任务的性能,然而本次重点解决的问题是如何更简易、更安全地获得的并发性。另外,Actor模型的应用实际上甚少,只在Scala中使用广泛,协程框架在Java中维护的也不成熟。这三者现阶段都不是足够的易用,也并不能解决业务上现阶段的问题。

      2. 追求参数设置合理性?

      有没有一种计算公式,能够让开发同学很简易地计算出某种场景中的线程池应该是什么参数呢?

      带着这样的疑问,我们调研了业界的一些线程池参数配置方案:

      调研了以上业界方案后,我们并没有得出通用的线程池计算方式。并发任务的执行情况和任务类型相关,IO密集型和CPU密集型的任务运行起来的情况差异非常大,但这种占比是较难合理预估的,这导致很难有一个简单有效的通用公式帮我们直接计算出结果。

      3. 线程池参数动态化?

      尽管经过谨慎的评估,仍然不能够保证一次计算出来合适的参数,那么我们是否可以将修改线程池参数的成本降下来,这样至少可以发生故障的时候可以快速调整从而缩短故障恢复的时间呢?基于这个思考,我们是否可以将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化前后的参数修改流程对比如下:

      图16 动态修改线程池参数新旧流程对比

      基于以上三个方向对比,我们可以看出参数动态化方向简单有效。

      3.3 动态化线程池

      3.3.1 整体设计

      动态化线程池的核心设计包括以下三个方面:

      1. 简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列 SynchronousQueue理解,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,Less is More。

      1)FixedThreadPool 为linkedBlockingQueue 和 SingleThreadPool:
      允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
      2)CachedThreadPool 为SynchronousQueue和 ScheduledThreadPool:
      允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

      • 参数可动态修改:为了解决参数不好配,修改参数成本高等问题。在Java线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。

      • 增加线程池监控:对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。

      • 图17 动态化线程池整体设计

        3.3.2 功能架构

        动态化线程池提供如下功能:

        • 动态调参:支持线程池参数动态调整、界面化操作;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效。

        • 任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;可以看到线程池的任务执行情况、最大任务执行时间、平均任务执行时间、95/99线等。

        • 负载告警:线程池队列任务积压到一定值的时候会通过大象(美团内部通讯工具)告知应用开发负责人;当线程池负载数达到一定阈值的时候会通过大象告知应用开发负责人。

        • 操作监控:创建/修改和删除线程池都会通知到应用的开发负责人。

        • 操作日志:可以查看线程池参数的修改记录,谁在什么时候修改了线程池参数、修改前的参数值是什么。

        • 权限校验:只有应用开发负责人才能够修改应用的线程池参数。

        图18 动态化线程池功能架构

        参数动态化

        JDK原生线程池ThreadPoolExecutor提供了如下几个public的setter方法,如下图所示:

        图19 JDK 线程池参数设置接口

        JDK允许线程池使用方通过ThreadPoolExecutor的实例来动态设置线程池的核心策略,以setCorePoolSize为方法例,在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idle的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务,setCorePoolSize具体流程如下:

        图20 setCorePoolSize方法执行流程

        线程池内部会处理好当前状态做到平滑修改,其他几个方法限于篇幅,这里不一一介绍。重点是基于这几个public方法,我们只需要维护ThreadPoolExecutor的实例,并且在需要修改的时候拿到实例修改其参数即可。基于以上的思路,我们实现了线程池参数的动态化、线程池参数在管理平台可配置可修改,其效果图如下图所示:

        图21 可动态修改线程池参数

        用户可以在管理平台上通过线程池的名字找到指定的线程池,然后对其参数进行修改,保存后会实时生效。目前支持的动态参数包括核心数、最大值、队列长度等。除此之外,在界面中,我们还能看到用户可以配置是否开启告警、队列等待任务告警阈值、活跃度告警等等。关于监控和告警,我们下面一节会对齐进行介绍。

        线程池监控

        除了参数动态化之外,为了更好地使用线程池,我们需要对线程池的运行状况有感知,比如当前线程池的负载是怎么样的?分配的资源够不够用?任务的执行情况是怎么样的?是长任务还是短任务?

        基于对这些问题的思考,动态化线程池提供了多个维度的监控和告警能力,包括:线程池活跃度、任务的执行Transaction(频率、耗时)、Reject异常、线程池内部统计信息等等,既能帮助用户从多个维度分析线程池的使用情况,又能在出现问题第一时间通知到用户,从而避免故障或加速故障恢复。

        1. 负载监控和告警

        线程池负载关注的核心问题是:基于当前线程池参数分配的资源够不够。对于这个问题,我们可以从事前和事中两个角度来看。事前,线程池定义了“活跃度”这个概念,来让用户在发生Reject异常之前能够感知线程池负载问题,线程池活跃度计算公式为:线程池活跃度 = activeCount/maximumPoolSize。这个公式代表当活跃线程数趋向于maximumPoolSize的时候,代表线程负载趋高。

        事中,也可以从两方面来看线程池的过载判定条件,一个是发生了Reject异常,一个是队列中有等待任务(支持定制阈值)。以上两种情况发生了都会触发告警,告警信息会通过大象推送给服务所关联的负责人。

        图22 大象告警通知

        2. 任务级精细化监控

        在传统的线程池应用场景中,线程池中的任务执行情况对于用户来说是透明的。比如在一个具体的业务场景中,业务开发申请了一个线程池同时用于执行两种任务,一个是发消息任务、一个是发短信任务,这两类任务实际执行的频率和时长对于用户来说没有一个直观的感受,很可能这两类任务不适合共享一个线程池,但是由于用户无法感知,因此也无从优化。动态化线程池内部实现了任务级别的埋点,且允许为不同的业务任务指定具有业务含义的名称,线程池内部基于这个名称做Transaction打点,基于这个功能,用户可以看到线程池内部任务级别的执行情况,且区分业务,任务监控示意图如下图所示:

        图23 线程池任务执行监控

        3. 运行时状态实时查看

        用户基于JDK原生线程池ThreadPoolExecutor提供的几个public的getter方法,可以读取到当前线程池的运行状态以及参数,如下图所示:

        图24 线程池实时运行情况

        动态化线程池基于这几个接口封装了运行时状态实时查看的功能,用户基于这个功能可以了解线程池的实时状态,比如当前有多少个工作线程,执行了多少个任务,队列中等待的任务数等等。效果如下图所示:

        图25 线程池实时运行情况

        3.4 实践总结

        面对业务中使用线程池遇到的实际问题,我们曾回到支持并发性问题本身来思考有没有取代线程池的方案,也曾尝试着去追求线程池参数设置的合理性,但面对业界方案具体落地的复杂性、可维护性以及真实运行环境的不确定性,我们在前两个方向上可谓“举步维艰”。

        最终,我们回到线程池参数动态化方向上探索,得出一个且可以解决业务问题的方案,虽然本质上还是没有逃离使用线程池的范畴,但是在成本和收益之间,算是取得了一个很好的平衡。成本在于实现动态化以及监控成本不高,收益在于:在不颠覆原有线程池使用方式的基础之上,从降低线程池参数修改的成本以及多维度监控这两个方面降低了故障发生的概率。希望本文提供的动态化线程池思路能对大家有帮助。

        四、参考资料

        [1]JDK 1.8 源码

        [2] 维基百科-线程池

        [3] 更好的使用Java线程池

        [4] 维基百科Pooling(Resource Management)

        [5] 深入理解Java线程池:ThreadPoolExecutor

        [6]《Java并发编程实践》

        五、作者简介

        致远,2018年加入美团点评,美团到店综合研发中心后台开发工程师。
        陆晨,2015年加入美团点评,美团到店综合研发中心后台技术专家。

        ----------  END  ----------

        招聘信息

        美团到店综合研发中心长期招聘前端、后端、数据仓库、机器学习/数据挖掘算法工程师,坐标上海,欢迎感兴趣的同学发送简历到:tech@meituan.com(邮件标题注明:美团到店综合研发中心—上海)

        也许你还想看

        Java 动态调试技术原理及实践

        Java字节码增强探秘

        Java魔法类:Unsafe应用解析

        Java动态追踪技术探究








        线程池你用过吧,线程数是怎么设置的呢?

        Hunter心想,这不难啊,曾经在《Java并发编程》一书中有看到过线程池中线程数目设置的讲述,于是张口就来:

        线程数的设置需要考虑三方面的因素,服务器的配置、服务器资源的预算和任务自身的特性。具体来说就是服务器有多少个CPU,多少内存,IO支持的最大QPS是多少,任务主要执行的是计算、IO还是一些混合操作,任务中是否包含数据库连接等的稀缺资源。线程池的线程数设置主要取决于这些因素。
        

        面试官追问来了:

        那具体是怎么设置呢?
        

        Hunter略一思忖,整理了下思路,娓娓道来:

        假设机器有N个CPU,那么对于计算密集型的任务,应该设置线程数为N+1;对于IO密集型的任务,应该设置线程数为2N;对于同时有计算工作和IO工作的任务,应该考虑使用两个线程池,一个处理计算任务,一个处理IO任务,分别对两个线程池按照计算密集型和IO密集型来设置线程数。
        

        面试官表情毫无变化,接着发问:

        N+1和2N是怎么来的?
        

        Hunter张口就来:

        是个经验值。
        

        面试官:

        经验值吗?那为什么不是N+2或者N+3,而非得是N+1呢?
        

        Hunter被驳得稍有点懵,脑子里努力在回想学习过的那些技术点,竟一时语塞。

        看得出来面试官略有不满,于是提示道:

        那假如在一个请求中,计算操作需要5ms,DB操作需要100ms,对于一台8个CPU的服务器,怎么设置线程数呢?
        

        Hunter努力平复心情,紧接着最开始的思路,说到:

        这是一个计算和IO混合型的任务,可以将其分解为两个线程池来处理。一个线程池处理计算操作,设置N+1=9个线程,一个线程处理IO操作,设置2N=16个线程。
        

        面试官:

        如果一个任务同时包含了一个计算操作和DB操作呢,不能拆分怎么设置?你能讲一下具体的计算过程吗?
        

        Hunter略有点慌,心里不断给自己暗示:这个问题不难不难。然后不断回想看过的《Java并发编程实战》和《Java虚拟机并发编程》中关于线程池设置的章节,并试图将自己对这个问题的分析思路也表达出来。

        首先这个任务整体上是一个IO密集型的任务。在处理一个请求的过程中,总共耗时100+5=105ms,而其中只有5ms是用于计算操作的,CPU利用率为5/(100+5)。使用线程池是为了尽量提高CPU的利用率,减少对CPU资源的浪费,假设以100%的CPU利用率来说,要达到100%的CPU利用率,对于一个CPU就要设置其利用率的倒数个数的线程数,也即1/(5/(100+5)),8个CPU的话就乘以8。那么算下来的话,就是……168,对,这个线程池要设置168个线程数。
        

        面试官表情略有缓和,嘴角微微一笑:

        如果实际的任务差异较大,不同任务实际的CPU操作耗时和IO操作耗时有所不同,那么怎么设置线程数呢?
        

        经过刚才的分析过程,Hunter心里已经回忆起了这块的知识点,已然不慌了。

        那对所有任务的CPU操作耗时和IO操作耗时求个平均值就好了。
        

        Hunter心里渐渐恢复了自信,大脑的利用率瞬间提高好几十个百分点。

        面试官轻轻“嗯”了一声,表示认可。

        那如果现在这个IO操作是DB操作,而DB的QPS上限是1000,这个线程池又该设置为多大呢?
        

        经过刚才的心理调整,对问题完整的分析过程,以及面试官的略微认可,Hunter已经知道如何去更好地回答面试官的问题了。

        按比例来减少就可以了,按照之前的计算过程,可以计算出来当线程数设置为168的时候,DB操作的QPS为,168(1000/(100+5))=1600,如果现在DB的QPS最大为1000,那么对应的,最大只能设置168(1000/1600)=105个线程。
        

        面试官这次是真的满意了,给这个回答给了一个正面的评价:

        思路挺清晰的。那设置线程池的时候除了考虑这些,还需要考虑哪些内容呢?
        

        Hunter此时已经完全找回自信了,不惧任何问题。

        除了考虑任务CPU操作耗时、IO操作耗时之外,还需要服务器的内存资源、硬盘资源、网络带宽等等的。
        

        面试官点点头,看起来Hunter已经获得了面试官的正式认可了。面试官告诉Hunter,表现不错,等接下来的面试安排吧。
        面试后总结

        Hunter内心异常激动,这真算是一次“死里逃生”的经历了。面试结束后,Hunter压抑兴奋,马上去找到《Java并发编程实战》和《Java虚拟机并发编程》两本书,翻到对应的章节,想确认下自己的回答。
        果然,压力除了会造成紧张之外,也能提高大脑利用率。Hunter在调整状态后的回答完全正确。附上两本书中对线程池设置的理论。
        线程数的第一种计算方法

        在《Java并发编程实践》中,是这样来计算线程池的线程数目的:

        在一个基准负载下,使用 几种不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。
        给定下列定义:
        Ncpu = CPU的数量
        Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
        W/C = 等待时间与计算时间的比率
        为保持处理器达到期望的使用率,最优的池的大小等于:
          Nthreads = Ncpu x Ucpu x (1 + W/C)
        

        这种计算方式,我们需要知道上面定义的几个数值,才能计算出来线程池需要设置的线程数。其中,CPU数量是确定的,CPU使用率是目标值也是确定的,W/C也是可以通过基准程序测试得出的。
        线程数的第二种计算方法

        而在《Java虚拟机并发编程》中,则是这样来计算线程池的线程数目的:

        线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间。
        计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。一个完全阻塞的任务是注定要挂掉的,所以我们无须担心阻塞系数会达到1。
        

        这种计算方式,我们需要知道CPU可用核心数和阻塞系数,才能计算出来线程池需要设置的线程数目。其中,CPU可用核心数是确定的,阻塞系数可以通过公式:阻塞系数=阻塞时间/(阻塞时间+计算时间),其实也就是上一种算法中的W/C的方式来计算,所以阻塞系数也是可以通过基准程序计算得出的。
        所谓的经验值怎么来的

        那么我们再来看所谓的N+1与2N的经验值的来源。
        计算密集型应用
        以第一种计算方式来看,对于计算密集型应用,假定等待时间趋近于0,是的CPU利用率达到100%,那么线程数就是CPU核心数,那这个+1意义何在呢?
        《Java并发编程实践》这么说:

        计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。
        

        所以N+1确实是一个经验值。
        IO密集型应用
        同样以第一种方式来看,对于IO密集型应用,假定所有的操作时间几乎都是IO操作耗时,那么W/C的值就为1,那么对应的线程数确实为2N。










        监控二

        摘要:背景在开发中,我们经常要使用Executors类创建线程池来执行大量的任务,使用线程池的并发特性提高系统的吞吐量。但是,线程池使用不当也会使服务器资源枯竭,导致异常情况的发生,比如固定线程池的阻塞队列任务数量过多、缓存线程池创建的线程过多导致内存溢出、系统假死等问题。因此,我们需要一种简单的监控方案来监控线程池的使用情况,比如完成任务数量、未完成任务数量、线程大小等信息。ExecutorsUtil工具类以下是我们开发的一个线程池工具类,该工具类扩展ThreadPoolExec
        背景 
        
        在开发中,我们经常要使用Executors类创建线程池来执行大量的任务,使用线程池的并发特性提高系统的吞吐量。但是,线程池使用不当也会使服务器资源枯竭,导致异常情况的发生,比如固定线程池的阻塞队列任务数量过多、缓存线程池创建的线程过多导致内存溢出、系统假死等问题。因此,我们需要一种简单的监控方案来监控线程池的使用情况,比如完成任务数量、未完成任务数量、线程大小等信息。
        
        ExecutorsUtil工具类 
        
        以下是我们开发的一个线程池工具类,该工具类扩展ThreadPoolExecutor实现了线程池监控功能,能实时将线程池使用信息打印到日志中,方便我们进行问题排查、系统调优。具体代码如下:
        
            package com.concurrent.monitor;
            import java.util.Date;
            import java.util.List;
            import java.util.concurrent.BlockingQueue;
            import java.util.concurrent.ConcurrentHashMap;
            import java.util.concurrent.ExecutorService;
            import java.util.concurrent.LinkedBlockingQueue;
            import java.util.concurrent.SynchronousQueue;
            import java.util.concurrent.ThreadFactory;
            import java.util.concurrent.ThreadPoolExecutor;
            import java.util.concurrent.TimeUnit;
            import java.util.concurrent.atomic.AtomicInteger;
            import org.slf4j.Logger;
            import org.slf4j.LoggerFactory;
            /**
             * 该类继承ThreadPoolExecutor类,覆盖了shutdown(), shutdownNow(), beforeExecute() 和 afterExecute() 
             * 方法来统计线程池的执行情况 
             *
             */
            public class ExecutorsUtil extends ThreadPoolExecutor {
                private static final Logger LOGGER = LoggerFactory.getLogger(ExecutorsUtil.class);
                // 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间 
                private ConcurrentHashMap startTimes;
                // 线程池名称,一般以业务名称命名,方便区分 
                private String poolName;
                /**
                 * 调用父类的构造方法,并初始化HashMap和线程池名称 
                 *
                 * @param corePoolSize
                 * 线程池核心线程数 
                 * @param maximumPoolSize
                 * 线程池最大线程数 
                 * @param keepAliveTime
                 * 线程的最大空闲时间 
                 * @param unit
                 * 空闲时间的单位 
                 * @param workQueue
                 * 保存被提交任务的队列 
                 * @param poolName
                 * 线程池名称 
                 */
                public ExecutorsUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue,
                                     String poolName) {
                    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new EventThreadFactory(poolName));
                    this.startTimes = new ConcurrentHashMap<>();
                    this.poolName = poolName;
                }
                /**
                 * 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计线程池情况 
                 */
                @Override
                public void shutdown() {
            // 统计已执行任务、正在执行任务、未执行任务数量 
                    LOGGER.info(String.format(this.poolName + " Going to shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
                            this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
                    super.shutdown();
                }
                /**
                 * 线程池立即关闭时,统计线程池情况 
                 */
                @Override
                public List shutdownNow() {
            // 统计已执行任务、正在执行任务、未执行任务数量 
                    LOGGER.info(
                            String.format(this.poolName + " Going to immediately shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
                                    this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
                    return super.shutdownNow();
                }
                /**
                 * 任务执行之前,记录任务开始时间 
                 */
                @Override
                protected void beforeExecute(Thread t, Runnable r) {
                    startTimes.put(String.valueOf(r.hashCode()), new Date());
                }
                /**
                 * 任务执行之后,计算任务结束时间 
                 */
                @Override
                protected void afterExecute(Runnable r, Throwable t) {
                    Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
                    Date finishDate = new Date();
                    long diff = finishDate.getTime() - startDate.getTime();
            // 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止 
                    LOGGER.info(String.format(this.poolName
                                    + "-pool-monitor: Duration: %d ms, PoolSize: %d, CorePoolSize: %d, Active: %d, Completed: %d, Task: %d, Queue: %d, LargestPoolSize: %d, MaximumPoolSize: %d,KeepAliveTime: %d, isShutdown: %s, isTerminated: %s",
                            diff, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(), this.getCompletedTaskCount(), this.getTaskCount(),
                            this.getQueue().size(), this.getLargestPoolSize(), this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS),
                            this.isShutdown(), this.isTerminated()));
                }
                /**
                 * 创建固定线程池,代码源于Executors.newFixedThreadPool方法,这里增加了poolName 
                 *
                 * @param nThreads
                 * 线程数量 
                 * @param poolName
                 * 线程池名称 
                 * @return ExecutorService对象
                 */
                public static ExecutorService newFixedThreadPool(int nThreads, String poolName) {
                    return new ExecutorsUtil(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue (), poolName);
                }
                /**
                 * 创建缓存型线程池,代码源于Executors.newCachedThreadPool方法,这里增加了poolName 
                 *
                 * @param poolName
                 * 线程池名称 
                 * @return ExecutorService对象
                 */
                public static ExecutorService newCachedThreadPool(String poolName) {
                    return new ExecutorsUtil(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue (), poolName);
                }
                /**
                 * 生成线程池所用的线程,只是改写了线程池默认的线程工厂,传入线程池名称,便于问题追踪 
                 */
                static class EventThreadFactory implements ThreadFactory {
                    private static final AtomicInteger poolNumber = new AtomicInteger(1);
                    private final ThreadGroup group;
                    private final AtomicInteger threadNumber = new AtomicInteger(1);
                    private final String namePrefix;
                    /**
                     * 初始化线程工厂 
                     *
                     * @param poolName
                     * 线程池名称 
                     */
                    EventThreadFactory(String poolName) {
                        SecurityManager s = System.getSecurityManager();
                        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
                        namePrefix = poolName + "-pool-" + poolNumber.getAndIncrement() + "-thread-";
                    }
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
                        if (t.isDaemon())
                            t.setDaemon(false);
                        if (t.getPriority() != Thread.NORM_PRIORITY)
                            t.setPriority(Thread.NORM_PRIORITY);
                        return t;
                    }
                }
            } 
        
        
        以上的ExecutorsUtil类继承了ThreadPoolExecutor类,重写了shutdown()、shutdownNow()、beforeExecute() 和 afterExecute() 方法来统计线程池的执行情况,这四个方法是ThreadPoolExecutor类预留给开发者进行扩展的方法,具体如下:
        
        shutdown():线程池延迟关闭时(等待线程池里的任务都执行完毕),统计已执行任务、正在执行任务、未执行任务数量 
        shutdownNow():线程池立即关闭时,统计已执行任务、正在执行任务、未执行任务数量 
        beforeExecute(Thread t, Runnable r):任务执行之前,记录任务开始时间,startTimes这个HashMap以任务的hashCode为key,开始时间为值 
        afterExecute(Runnable r, Throwable t):任务执行之后,计算任务结束时间。统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止信息 
        
        监控到的记录如下:
        
        momentspush-pool-2-thread-90 ExecutorsUtil.java:91 momentspush_monitor: Duration: 599 ms, PoolSize: 200, CorePoolSize: 200, Active: 200, Completed: 334924, Task: 417702, Queue: 82578, LargestPoolSize: 200, MaximumPoolSize: 200,KeepAliveTime: 0, isShutdown: false, isTerminated: false 
        
        一般我们会依赖beforeExecute和afterExecute这两个方法统计的信息,具体原因请参考需要注意部分的最后一项。有了这些信息之后,我们可以根据业务情况和统计的线程池信息合理调整线程池大小,根据任务耗时长短对自身服务和依赖的其他服务进行调优,提高服务的可用性。
        
        需要注意的 
        在afterExecute方法中需要注意,需要调用ConcurrentHashMap的remove方法移除并返回任务的开始时间信息,而不是调用get方法,因为在高并发情况下,线程池里要执行的任务很多,如果只获取值不移除的话,会使ConcurrentHashMap越来越大,引发内存泄漏或溢出问题。该行代码如下: 
        Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
        
        有了ExecutorsUtil类之后,我们可以通过newFixedThreadPool(int nThreads, String poolName)和newCachedThreadPool(String poolName)方法创建两个日常我们使用最多的线程池,跟默认的Executors里的方法不同的是,这里需要传入poolName参数,该参数主要是用来给线程池定义一个与业务相关并有具体意义的线程池名字,方便我们排查线上问题。具体可参考《Java线程池扩展之关联线程池与业务》一文
        
        在生产环境中,谨慎调用shutdown()和shutdownNow()方法,因为调用这两个方法之后,线程池会被关闭,不再接收新的任务,如果有新任务提交到一个被关闭的线程池,会抛出java.util.concurrent.RejectedExecutionException异常,具体可参考《一步一步分析RejectedExecutionException异常》一文。其实在使用Spring等框架来管理类的生命周期的条件下,也没有必要调用这两个方法来关闭线程池,线程池的生命周期完全由该线程池所属的Spring管理的类决定
        
        转者实践代码:
        
        import java.util.Date;
        import java.util.List;
        import java.util.concurrent.*;
        import java.util.concurrent.atomic.AtomicInteger;
        import org.slf4.LoggerFactory;
        import org.slf4j.Logger;
         
        /**
         * 参考 https://www.aliyun.com/jiaocheng/778174.html
         * 该类继承ThreadPoolExecutor类,覆盖了shutdown(), shutdownNow(), beforeExecute() 和 afterExecute()
         * 方法来统计线程池的执行情况
         */
        public class ThreadPoolExecutorWrapper extends ThreadPoolExecutor {
            private static final Logger THREAD_POOL_MONITOR_LOGGER = LoggerFactory.getLogger("THREAD_POOL_MONITOR_LOGGER");
            // 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间
            private ConcurrentHashMap<String,Date> startTimes;
            // 线程池名称,一般以业务名称命名,方便区分
            private String poolName;
         
            /**
             * 调用父类的构造方法,并初始化HashMap和线程池名称
             *
             * @param corePoolSize    线程池核心线程数
             * @param maximumPoolSize 线程池最大线程数
             * @param keepAliveTime   线程的最大空闲时间
             * @param unit            空闲时间的单位
             * @param queueCapacity       保存被提交任务的队列
             * @param poolName        线程池名称
             */
            public ThreadPoolExecutorWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, int queueCapacity,
                                             String poolName, RejectedExecutionHandler rejectedExecutionHandler) {
                super(corePoolSize, maximumPoolSize, keepAliveTime, unit, new LinkedBlockingQueue<Runnable>(queueCapacity), new EventThreadFactory(poolName),rejectedExecutionHandler);
                this.startTimes = new ConcurrentHashMap<>();
                this.poolName = poolName;
            }
         
         
         
         
            /**
             * 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计线程池情况
             */
            @Override
            public void shutdown() {
            // 统计已执行任务、正在执行任务、未执行任务数量
                THREAD_POOL_MONITOR_LOGGER.info(String.format(this.poolName + " Going to shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
                        this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
                super.shutdown();
            }
         
            /**
             * 线程池立即关闭时,统计线程池情况
             */
            @Override
            public List shutdownNow() {
            // 统计已执行任务、正在执行任务、未执行任务数量
                THREAD_POOL_MONITOR_LOGGER.info(
                        String.format(this.poolName + " Going to immediately shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
                                this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
                return super.shutdownNow();
            }
         
            /**
             * 任务执行之前,记录任务开始时间
             */
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                startTimes.put(String.valueOf(r.hashCode()), new Date());
            }
         
            /**
             * 任务执行之后,计算任务结束时间
             */
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
                if(startDate == null){
                    startDate = new Date(0L);
                }
                Date finishDate = new Date();
                long diff = finishDate.getTime() - startDate.getTime();
                // 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止
                THREAD_POOL_MONITOR_LOGGER.info(String.format(this.poolName
                                + "-pool-monitor: Duration: %d ms, PoolSize: %d, CorePoolSize: %d, Active: %d, Completed: %d, Task: %d, Queue: %d, LargestPoolSize: %d, MaximumPoolSize: %d,KeepAliveTime: %d, isShutdown: %s, isTerminated: %s",
                        diff, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(), this.getCompletedTaskCount(), this.getTaskCount(),
                        this.getQueue().size(), this.getLargestPoolSize(), this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS),
                        this.isShutdown(), this.isTerminated()));
         
               }
         
         
         
            /**
             * 生成线程池所用的线程,只是改写了线程池默认的线程工厂,传入线程池名称,便于问题追踪
             */
            static class EventThreadFactory implements ThreadFactory {
                private static final AtomicInteger poolNumber = new AtomicInteger(1);
                private final ThreadGroup group;
                private final AtomicInteger threadNumber = new AtomicInteger(1);
                private final String namePrefix;
         
                /**
                 * 初始化线程工厂
                 *
                 * @param poolName 线程池名称
                 */
                EventThreadFactory(String poolName) {
                    SecurityManager s = System.getSecurityManager();
                    group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
                    namePrefix = poolName + "-pool-" + poolNumber.getAndIncrement() + "-thread-";
                }
         
                @Override
                public Thread newThread(Runnable r) {
                    Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
                    if (t.isDaemon())
                        t.setDaemon(false);
                    if (t.getPriority() != Thread.NORM_PRIORITY)
                        t.setPriority(Thread.NORM_PRIORITY);
                    return t;
                }
            }
        }
        
        spring配置一个线程池实例
        <!-- test -->
            <bean id="myTaskExecutor" class="*.util.ThreadPoolExecutorWrapper">
               <constructor-arg type="int" value="8"/><!-- corePoolSize 线程池维护线程的最少数量 -->
               <constructor-arg type="int" value="20"/><!-- maximumPoolSize-->
               <constructor-arg type="long" value="60"/><!--keep alive time-->
               <constructor-arg type="java.util.concurrent.TimeUnit" value="SECONDS"/>
               <constructor-arg type="int" value="5000" /><!--work queue capacity-->
                <constructor-arg type="java.lang.String" value="myTask" />
                <constructor-arg type="java.util.concurrent.RejectedExecutionHandler" ref="abortPolicy"/>
            </bean>
         
            <bean id ="abortPolicy" class="java.util.concurrent.ThreadPoolExecutor$AbortPolicy" />
        
        logger4j配置 
        <logger name="THREAD_POOL_MONITOR_LOGGER" additivity="false">
                <level value="INFO"/>
                <appender-ref ref="THREAD_POOL_MONITOR_APPENDER"/>
            </logger>
         
            <appender name="THREAD_POOL_MONITOR_APPENDER" class="org.apache.log4j.DailyRollingFileAppender">
                <param name="File" value="/app/logs/test/monitor/threadpool.log"/>
                <param name="Append" value="true"/>
                <param name="DatePattern" value="'.'yyyy-MM-dd"/>
                <layout class="org.apache.log4j.PatternLayout">
                    <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %m%n"/>
                </layout>
            </appender>
        

        日志

        2018-10-16 20:51:02 my_task_pool-pool-monitor: Duration: 12 ms, PoolSize: 8, CorePoolSize: 8, Active: 2, Completed: 2998, Task: 3000, Queue: 0, LargestPoolSize: 8, MaximumPoolSize: 20,KeepAliveTime: 60000, isShutdown: false, isTerminated: false
         
        2018-10-16 20:51:02 my_task_pool-pool-monitor: Duration: 12 ms, PoolSize: 8, CorePoolSize: 8, Active: 2, Completed: 2998, Task: 3000, Queue: 0, LargestPoolSize: 8, MaximumPoolSize: 20,KeepAliveTime: 60000, isShutdown: false, isTerminated: false
        

        转自 https://www.aliyun.com/jiaocheng/778174.html

        监控三*

        美团点评 Cat监控 线程池监控
        mrjyng 2020-03-14 16:38:38 654 已收藏
        分类专栏: 并发 分布式 java 文章标签: java 监控类
        版权

          在日常项目开发中,我们经常会遇到需要异步处理的任务,例如日志服务,监控服务等。有一定开发经验的同学首先就会想到使用线程池,因为“在线程池中执行任务”比“为每个任务创建一个线程”更有优势,通过重用现有的线程,可以避免线程在不断创建、销毁过程中产生的开销。在java开发中,一般做法就是基于ThreadPoolExecutor类,自定义corePoolSize, maxPoolSize, ThreadFactory等参数来创建一个线程池工具类。但是,线程池也不是没有缺点的,对于开发者而言,我们很难在项目上线之前准确地预估业务的规模,所以,如何合理地为线程池设置corePoolSize,maxPoolSize是一个比较难把握的事情,设置不合理会导致不能达到预计的性能,甚至会引发线上故障。不过,利用ThreadPoolExecutor为我们提供的一些监控api,我们可以做到对线程池进行实时监控和调优。本文将利用如下几个API,并结合Cat提供的拓展功能,实现对线程池的持续监控。
        

        1.自定义线程池

        如下代码是我用来测试的一个线程池工具类

        public class ThreadPoolManager<T> {
         
            /**
             * 根据cpu的数量动态的配置核心线程数和最大线程数
             */
            //private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
            /**
             * 核心线程数 = CPU核心数 + 1
             */
            private static final int CORE_POOL_SIZE = 20;
            /**
             * 线程池最大线程数 = CPU核心数 * 2 + 1
             */
            private static final int MAXIMUM_POOL_SIZE = 25;
         
            private static final int QUEUE_SIZE = 1000;
         
            /**
             * 非核心线程闲置时超时1s
             */
            private static final int KEEP_ALIVE = 3;
         
         
            /**
             * 线程池的对象
             */
            private final ThreadPoolExecutor executor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
                    KEEP_ALIVE, TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_SIZE),
                    Executors.defaultThreadFactory(), new DiscardPolicyWithLog());
         
            /**
             * 要确保该类只有一个实例对象,避免产生过多对象消费资源,所以采用单例模式
             */
            private ThreadPoolManager() {
                
            }
         
            private static ThreadPoolManager sInstance;
         
         
            public static ThreadPoolManager getsInstance() {
                if (sInstance == null) {
                    synchronized (ThreadPoolManager.class) {
                        if (sInstance == null) {
                            sInstance = new ThreadPoolManager();
                        }
                    }
                }
                return sInstance;
            }
         
            /**
             * 开启一个无返回结果的线程
             * @param r
             */
            public void execute(Runnable r) {
                executor.execute(r);
            }
         
            /**
             * 开启一个有返回结果的线程
             *
             * @param r
             * @return
             */
            public Future<T> submit(Callable<T> r) {
                // 把一个任务丢到了线程池中
                return executor.submit(r);
            }
         
            /**
             * 把任务移除等待队列
             *
             * @param r
             */
            public void cancel(Runnable r) {
                if (r != null) {
                    executor.getQueue().remove(r);
                }
            }
        }
        

        2.加入Cat监控

        假设你的项目已经集成了cat监控,那么,利用cat提供的SPI拓展能力,我们可以监控任何需要关注的系统运行时指标。我们现在想要监控线程池运行时的一些指标,帮助我们更好的优化线程池配置,提高系统性能。在以上线程池工具类的构造方法里加入如下代码,就可以实现。
        
        private ThreadPoolManager() {
                StatusExtensionRegister.getInstance().register(new StatusExtension() {
                    @Override
                    public String getId() {
                        return "mqtt_msg_pool_monitor";
                    }
         
                    @Override
                    public String getDescription() {
                        return "mqtt消息处理线程池监控";
                    }
         
                    @Override
                    public Map<String, String> getProperties() {
                        Map<String,String> map = new HashMap<>();
                        //线程池曾经创建过的最大线程数量
                        map.put("largest-pool-size", String.valueOf(executor.getLargestPoolSize()));
                        map.put("max-pool-size", String.valueOf(executor.getMaximumPoolSize()));
                        map.put("core-pool-size", String.valueOf(executor.getCorePoolSize()));
                        map.put("current-pool-size", String.valueOf(executor.getPoolSize()));
                        map.put("queue-size", String.valueOf(executor.getQueue().size()));
         
                        return map;
                    }
                });
            }
        
        1. 效果观察
          在这里插入图片描述






        *四 计算时间
        具体实践

        通过公式,我们了解到需要 3 个具体数值

        一个请求所消耗的时间 (线程 IO time + 线程 CPU time)
        
        该请求计算时间 (线程 CPU time)
        
        CPU 数目
        

        请求消耗时间

        Web 服务容器中,可以通过 Filter 来拦截获取该请求前后消耗的时间

        public class MoniterFilter implements Filter {

        private static final Logger logger = LoggerFactory.getLogger(MoniterFilter.class);

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
        ServletException {
        long start = System.currentTimeMillis();

            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            String uri = httpRequest.getRequestURI();
            String params = getQueryString(httpRequest);
        

        try {
        chain.doFilter(httpRequest, httpResponse);
        } finally {
        long cost = System.currentTimeMillis() - start;
        logger.info(“access url [{}{}], cost time [{}] ms )”, uri, params, cost);
        }

        private String getQueryString(HttpServletRequest req) {
        StringBuilder buffer = new StringBuilder("?");
        Enumeration emParams = req.getParameterNames();
        try {
            while (emParams.hasMoreElements()) {
        String sParam = emParams.nextElement();
        String sValues = req.getParameter(sParam);
        buffer.append(sParam).append("=").append(sValues).append("&");
        }
        return buffer.substring(0, buffer.length() - 1);
        } catch (Exception e) {
        logger.error(“get post arguments error”, buffer.toString());
        }
        return “”;
        }
        }

        CPU 计算时间

        CPU 计算时间 = 请求总耗时 - CPU IO time
        

        假设该请求有一个查询 DB 的操作,只要知道这个查询 DB 的耗时(CPU IO time),计算的时间不就出来了嘛,我们看一下怎么才能简洁,明了的记录 DB 查询的耗时。

        通过(JDK 动态代理/ CGLIB)的方式添加 AOP 切面,来获取线程 IO 耗时。代码如下,请参考:

        public class DaoInterceptor implements MethodInterceptor {

        private static final Logger logger = LoggerFactory.getLogger(DaoInterceptor.class);

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
        StopWatch watch = new StopWatch();
        watch.start();
        Object result = null;
        Throwable t = null;
        try {
        result = invocation.proceed();
        } catch (Throwable e) {
        t = e == null ? null : e.getCause();
        throw e;
        } finally {
        watch.stop();
        logger.info("({}ms)", watch.getTotalTimeMillis());
        }
        return result;
        }

        }

        CPU 数目

        逻辑 CPU 个数 ,设置线程池大小的时候参考的 CPU 个数

        cat /proc/cpuinfo| grep “processor”| wc -l

        总结

        合适的配置线程池大小其实很不容易,但是通过上述的公式和具体代码,我们就能快速、落地的算出这个线程池该设置的多大。

        不过最后的最后,我们还是需要通过压力测试来进行微调,只有经过压测测试的检验,我们才能最终保证的配置大小是准确的。*







        五监控

        springboot实现异步线程池并实现实时监控

        背景

        :因为我要对接京东订单服务 拉取订单的时候需要100个商户同时拉取订单服务,必须是异步的。

        首先要在springboot 启动处加入

         

        @EnableAsync
        @Configuration
        class TaskPoolConfig {
            @Bean("taskExecutor")
            public Executor taskExecutor() {
                //注意这一行日志:2. do submit,taskCount [101], completedTaskCount [87], activeCount [5], queueSize [9]
                //这说明提交任务到线程池的时候,调用的是submit(Callable task)这个方法,当前已经提交了101个任务,完成了87个,当前有5个线程在处理任务,还剩9个任务在队列中等待,线程池的基本情况一路了然;
                ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
                //核心线程数10:线程池创建时候初始化的线程数
                executor.setCorePoolSize(10);
                 //最大线程数20:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
                 //maxPoolSize 当系统负载大道最大值时,核心线程数已无法按时处理完所有任务,这是就需要增加线程.每秒200个任务需要20个线程,那么当每秒1000个任务时,则需要(1000-queueCapacity)*(20/200),即60个线程,可将maxPoolSize设置为60;
                 executor.setMaxPoolSize(30);
                //缓冲队列200:用来缓冲执行任务的队列
                executor.setQueueCapacity(400);
                //允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
                executor.setKeepAliveSeconds(60);
                //线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
                executor.setThreadNamePrefix("taskExecutor");
                //理线程池对拒绝任务的处策略:这里采用了CallerRunsPolicy策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
                /*CallerRunsPolicy:线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
                这个策略显然不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。(开始我总不想丢弃任务的执行,但是对某些应用场景来讲,很有可能造成当前线程也被阻塞。如果所有线程都是不能执行的,很可能导致程序没法继续跑了。需要视业务情景而定吧。)
                AbortPolicy:处理程序遭到拒绝将抛出运行时 RejectedExecutionException
                这种策略直接抛出异常,丢弃任务。(jdk默认策略,队列满并线程满时直接拒绝添加新任务,并抛出异常,所以说有时候放弃也是一种勇气,为了保证后续任务的正常进行,丢弃一些也是可以接收的,记得做好记录)
                DiscardPolicy:不能执行的任务将被删除
                这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。
                DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
                该策略就稍微复杂一些,在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务。这个策略需要适当小心*/
                executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
                executor.setWaitForTasksToCompleteOnShutdown(true);
                executor.setAwaitTerminationSeconds(60);
                return executor;
            }
        }
        

        每个配置文件代表什么意思可以看一下

        这个时候启动的时候我们异步线程池是已经创建好

        我们创建一个task 类

        public class Task {
            public static Random random = new Random();
        
        @Async("taskExecutor")
        public void doTask(Integer i) throws Exception {
            System.out.println("开始做任务");
            long start = System.currentTimeMillis();
            //这里写业务代码
            long end = System.currentTimeMillis();
            System.out.println("完成任务耗时:" + (end - start)/1000 + "秒");
        
        }</pre> 
        

        }

        这个时候我们就可以使用了我们把task 注入到 controller 层

        @RestController
        @RequestMapping("test/")
        public class TestController {
        
        @Autowired
        private TaskService taskService;
        
        @Autowired
        private Executor taskExecutor;
        
        
        private Logger logger = LogManager.getLogger(JDcontroller.class);
        
        @PostMapping("order")
        public String addOrder(@RequestBody RequestParameterDTO requestParameters){
                //这里会执行你开启的任务,都是异步的,调用这个接口会立马返回 OK  然后业务是在后台运行的
                taskService.doTask(requestParameters);
                return "OK"; 
        }
        //这里我们可以通过接口实时观看效果 具体效果如下图
        @GetMapping("order/asyncExceutor")
        public Map getThreadInfo() {
            Map map =new HashMap();
            Object[] myThread = {taskExecutor};
            for (Object thread : myThread) {
                ThreadPoolTaskExecutor threadTask = (ThreadPoolTaskExecutor) thread;
                ThreadPoolExecutor threadPoolExecutor =threadTask.getThreadPoolExecutor();
                System.out.println("提交任务数"+threadPoolExecutor.getTaskCount());
                System.out.println("完成任务数"+threadPoolExecutor.getCompletedTaskCount() );
                System.out.println("当前有"+threadPoolExecutor.getActiveCount()+"个线程正在处理任务");
                System.out.println("还剩"+threadPoolExecutor.getQueue().size()+"个任务");
                map.put("提交任务数--&gt;",threadPoolExecutor.getTaskCount());
                map.put("完成任务数--&gt;",threadPoolExecutor.getCompletedTaskCount());
                map.put("当前有多少线程正在处理任务--&gt;",threadPoolExecutor.getActiveCount());
                map.put("还剩多少个任务未执行--&gt;",threadPoolExecutor.getQueue().size());
                map.put("当前可用队列长度--&gt;",threadPoolExecutor.getQueue().remainingCapacity());
                map.put("当前时间--&gt;",DateFormatUtil.stringDate());
            }
            return map;
        }
        

        }

         

        上图那个名字要和你在springboot启动处定义的名字要相同 这样spring 才能找到 才能监控你的线程池 ,当然这做的好处是你可以监控多个线程池 的线程,只需要在启动处 在加入  类似 的代码,名字不一样就行了

         

      • 1
        点赞
      • 1
        收藏
        觉得还不错? 一键收藏
      • 0
        评论

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

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

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

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

      抵扣说明:

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

      余额充值