深度解析线程池ThreadPoolExecutor

1. 什么是线程池,为什么要使用线程池?

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

2. ThreadPoolExecutor的继承关系

                                         Executor
                                            ↑
                                      ExecutorService
                                            ↑
                                   AbstractExecutorService
                                            ↑
                                     ThreadPoolExecutor

Executor:仅有一个execute方法,提供了一种思想:将任务提交和任务执行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。

ExecutorService:增加了一些能力:1.提供了管控线程池的方法,比如启动、停止、唤醒线程池。2.扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法。

AbstractExecutorService:上层的抽象类,提供了部分方法的实现,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。

ThreadPoolExecutor:实现最复杂的运行部分,ThreadPoolExecutor一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

3. 线程池如何维护自身的生命周期?线程池如何管理线程?线程池如何管理任务?

3.1 线程池如何维护自身的生命周期?

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

线程池内部使用一个变量ctl维护两个值:运行状态(runState)和线程数量(countBits)
高3位保存runState,低29位保存workerCount。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。
ThreadPoolExecutor的运行状态有五种:

名称 状态值 描述
RUNNING -1 能接受新提交的任务,并且也能处理阻塞队列中的任务
SHUTDOWN 0 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务
STOP 1 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程
TIDING 2 所有的任务都已终止,workerCount(有效线程数)为0
TERMINATED 3 在terminated()方法执行完后进入该状态

3.1.1为什么要用一个变量去存储两个值?

可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。

3.2 线程池如何管理任务?

3.2.1 任务调度

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

  public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *  1.如果工作线程小于核心线程数,尝试新建一个工作线程执行任务addWorker。
         * addWorker将会自动检查线程池状态和工作线程数,以防在添加工作线程的过程中,线程池被关闭。
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *  2.如果创建工作线程执行任务失败,则任务入队列,如果入队列成功,我们仍需要二次检查线程池状态,以防在入队列的过程中,线程池关闭。如果线程池关闭,则回滚任务。
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         *  3.如果任务入队列失败,则尝试创建一个工作线程执行任务
         */
        int c = ctl.get();
        //如果当前工作线程数小于核心线程数,则添加新的工作线程执行任务。
        if (workerCountOf(c) < corePoolSize) {    
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果当前工作线程数大于等于核心线程数,检查线程池运行状态,若为正在运行,则添加任务到任务队列。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //重新检查线程池运行状态,如果线程池处于非运行状态,则移除任务。
            if (! isRunning(recheck) && remove(command))
                //移除成功则进行拒绝任务处理
                reject(command);
            //如果工作线程为0(之前的线程已被销毁完),则创建一个工作线程。
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //false表示判断最大线程数,此处为根据最大线程数,判断是否应该添加工作线程,如果当前工作线程数量小于最大线程数,则尝试添加工作线程执行任务,如果执行失败,则拒绝任务处理
        else if (!addWorker(command, false))
            reject(command);
    }

addWorker方法源码如下:

  /**
    *  根据当前线程池状态和核心线程数量与最大线程数量,检查是否应该添加工作线程执行任务。
    *  如果应该添加工作线程,则更新工作线程数,如果调整成功,则创建工作线程,执行任务。
    *  如果线程是已关闭或正在关闭,则添加工作线程失败。如果线程工厂创建线程失败,则返回false,如果由于线程工厂返回null或OutOfMemoryError等原因,则执行回滚清除操作。
    */
 private boolean addWorker(Runnable firstTask, boolean core) {
        retry:  
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            // Check if queue empty only if necessary.
            //如果线程池非运行状态 且 不同时满足线程池处于关闭状态,提交的任务为null,任务队列不为空,则直接返回false,添加工作线程失败。
            //SHUTDOWN状态不接受新任务,但仍然会执行已经加入任务队列的任务。
            //所以当进入SHUTDOWN状态,而传进来的任务为空,且任务队列不为空的时候,是允许添加新线程的,如果把这个条件取反,就表示不允许添加worker。
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;
            
            for (;;) {
                int wc = workerCountOf(c);
                //如果工作线程数量大于线程池容量,或当前工作线程大于core,或当前工作线程数量大于core(传入参数core为true时,此处core为corePoolSize,否则为maximumPoolSize),则直接返回false,添加工作线程失败
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //CAS操作工作线程数,即原子操作工作线程数+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
            }
        }
        
        //上面这段代码主要是对worker数量做原子+1操作。下面的逻辑才是正式构建Worker

        boolean workerStarted = false;//工作线程是否开始
        boolean workerAdded = false;//工作线程是否添加成功
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                //这里有个重入锁,避免并发问题
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());
                    
                    //如果线程池是正在运行或线程池已关闭状态同时任务为null,则添加工作线程到工作线程集
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {  
                        //任务刚封装到work里面,还没start,线程状态不可能是alive,故抛出异常。
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //添加工作线程到工作线程集
                        workers.add(w);
                        int s = workers.size();
                        //如果集合中的工作线程数大于最大线程数,这个最大线程数表示线程池曾经出现过的最大线程数。则更新线程池出现过的最大线程数
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                //添加工作线程成功,则启动线程执行任务
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            //执行任务失败,则回滚工作线程和工作线程数
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

总结而言,线程池的执行过程如下:

  1. 检查线程池运行状态
  2. 如果workCount < corePoolSize,创建并启动一个线程来执行新提交的任务
  3. 如果workCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workCount >=corePoolSize && workCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workCount >= maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务,默认的处理方式是直接抛异常。

任务缓冲

线程池以生产者消费者模式,通过一个阻塞队列来实现。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
常用阻塞队列如下表

名称 描述
ArrayBlockingQueue 一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。
LinkedBlockingQueue 一个由链表结构组成的有界队列,此队列按照先进先出(FIFO)的原则对元素进行排序。此队列的默认长度为Integer.MAX_VALUE。所以默认创建的该队列有容量危险。
PriorityBlockingQueue 一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。
DelayQueue 一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。
SynchronousQueue 一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程就会重复使用,线程空闲了60秒后会被回收。
LinkedTransferQueue 一个由链表结构构成的无界阻塞队列,相对于其他队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。
LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。

任务申请

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

线程需要从任务缓存模块中不断地取任务执行,帮助线程从高阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。
这部分策略由getTask方法实现。
getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。

任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时i,就需要拒绝掉该任务,采用任务拒绝策略,保护线程池。
用户可以通过实现RejectedExecutionHandler接口定制拒绝策略。也可以选择JDK提供的四种拒绝策略。

名称 描述
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但不抛出异常。使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。
ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列最前面的任务,然后重新提交被拒绝的任务。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
ThreadPoolExecutor.CallerRunPolicy 由调用线程(提交任务的线程)处理该任务。这种情况是需要让所有任务都执行完毕。那么就适合大量计算的任务类型去执行,多线程仅仅是增大吞吐量的手段,最终必须要让每个任务都执行完毕。

线程池如何管理线程?

工作线程Worker

线程池为了掌握线程的状态,并维护线程的生命周期,设计了线程池内工作线程Worker管理线程。

/** 
  *  Worker主要为任务线程维护中断控制状态和其他次要状态记录。Worker简单实现了AQS在任务线程执行前lock,任务执行完unlock。加锁的主要目的是保护任务线程的执行。
  *  线程池唤醒一个任务线程等待任务,而不是中断当前正在执行任务的线程去执行任务。我们使用了一个非重入互斥锁而不是ReentrantLock,
  *  这样做的目的是以防在任务执行的过程,线程池控制方法的改变,对任务线程执行的影响,比如setCorePoolSize方法。
  *  另外为了防止任务线程在实际执行前被中断,我们初始化锁状态为-1,在runWorker方法中,我们会清除它。
  */
private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        /**
         * This class will never be serialized, but we provide a
         * serialVersionUID to suppress a javac warning.
         */
        private static final long serialVersionUID = 6138294804551838833L;

        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;  //任务线程,正正执行task的线程
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;  //任务
        /** Per-thread task counter */
        volatile long completedTasks;  //线程完成的任务计数

        /**
         * Creates with given first task and thread from ThreadFactory.
         * 根据给定的任务,用线程工厂创建任务线程
         * @param firstTask the first task (null if none)
         */
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker  */
        public void run() {
            runWorker(this);
        }

        // Lock methods
        //
        // The value 0 represents the unlocked state.
        // The value 1 represents the locked state.
        //  判断是否锁状态 0为闭锁状态,1为开锁状态
        protected boolean isHeldExclusively() {
            return getState() != 0;
        }
         
        //尝试获取锁
        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        
        //尝试释放锁
        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }

        void interruptIfStarted() {
            Thread t;
            //如果锁处于关闭状态,且任务线程不为null,非处于中断状态,则中断当前线程
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

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

什么是AQS?

AQS:AbstractQueuedSynchronizer 抽象的队列式的同步器
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类的实现都依赖于它,如常用的ReentrantLock、Semaphore等。
AQS的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。

总结:AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

为什么要继承AQS?

线程池需要管理线程的生命周期,需要在线程池长时间不运行的时候进行回收。

  private final HashSet<Worker> workers = new HashSet<Worker>();

线程池使用一张HashSet去持有线程的引用,这样可以通过添加引用,移除引用这样的操作来控制线程的生命周期。
此时Work需要提供对线程是否在运行的判断。
故Worker通过继承AQS,使用AQS来实现独占锁的功能,没有使用可重入锁ReeentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态
lock方法一旦获取了独占锁,表示当前线程正在执行任务中,那么它会有以下几个作用:
1.如果正在执行任务,则不应该中断线程。
2.如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
3.线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态。
4.之所以设置为不可重入,是因为我们不希望任务在调用像 setCorePoolSize 这样的线程池控制方法时重新获取锁,这样会中断正在运行的线程

Worker线程增加

增加线程是通过线程池中的addWorker方法。该方法的功能就是增加一个线程。(源码见上文)

Worker线程回收

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

Worker线程执行任务

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

  1. while循环不断地通过getTask()方法获取任务。
  2. getTask()方法从阻塞队列中取任务。
  3. 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
  4. 执行任务
  5. 如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。
  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.
            /* 对线程池状态的判断,两种情况会workCount-1,并且返回null
               1. 线程池状态为shutdown,且workQueue为空(反映了shutdown状态的线程池还是要执行workQueue中剩余的任务)
               2.  线程池状态为stop(shutdownNow()会导致变成stop),此时不考虑workQueue的情况
             */
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);
            
            // Are workers subject to culling?
            //如果线程池正在运行,根据是否允许空闲线程等待任务和当前工作线程与核心线程数比较值,判断是否需要超时等待任务。
            //allowCoreThreadTimeOut默认为false,也就是默认核心线程不允许进行超时。
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            //如果当前工作线程数,小于最大线程数,空闲工作线程不需要超时等待任务,则跳出自旋,即在当前工作线程小于最大线程数的情况下,有工作线程可用,任务队列为空。
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
            
            /*根据 timed 来判断,如果为 true,则通过阻塞队列 poll 方法进行超时控制,如果在keepaliveTime 时间内没有获取到任务,则返回 null.
                否则通过 take 方法阻塞式获取队列中的任务*/
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)  //如果拿到的任务不为空,则直接返回给worker进行处理
                    return r;
                timedOut = true;  //如果 r==null,说明已经超时了,设置 timedOut=true,在下次自旋的时候进行回收
            } catch (InterruptedException retry) {
                timedOut = false;  // 如果获取任务时当前线程发生了中断,则设置 timedOut 为false 并返回循环重试
            }
        }
    }

线程池构造参数:

  1. corePoolSize:线程池核心线程大小
  2. maximumPoolSize:线程池最大线程数量
  3. keepAliveTime:空闲线程存活时间
  4. unit:空闲线程存活时间单位
  5. workQueue:工作队列
  6. threadFactory:线程工厂
  7. handler:拒绝策略

线程池扩展优化方案

tomcat对ThreadPoolExecutor的扩展

通常我们可以将执行的任务分为两类:
1.CPU密集型任务:需要线程长时间进行复杂的运算,这种类型的任务需要少创建线程,过多的线程会频繁引起上文切换,降低任务处理速度。
2.IO密集型任务,由于线程并不是一直在运行,可能大部分时间在等待IO读取/写入数据,增加线程数量可以提高并发度,尽可能多处理任务。

原生线程池队列未满之前,最多只有核心线程数量线程,这种策略比较适合处理CPU密集型任务,但是对于IO密集型任务,如数据库查询,PRC请求调用等,就不是很友好了。

由于Tomcat需要处理大量客户端请求任务,如果采用原生线程池,一旦接受请求大于线程池核心线程数,这些请求就会被放入到队列中,等待核心线程处理。这样做显然降低这些请求总体处理速度,所以对原生JDK线程池进行了扩展。

tomcat的ThreadPoolExecutor直接继承自JUC的ThreadPoolExecutor,进行了以下扩展:
1.自定义了一个AtomicInteger的计数器submittedCount,计算提交的线程数。
这个变量实时统计已经提交到线程池中,但还没有执行结束的任务。
即该值等于线程池队列中的任务数加上线程池工作线程正在执行的任务数。

2.提供了多个构造器,直接调用父类构造注入属性,同时执行prestartAllCoreThread()方法

  /**
     * Starts all core threads, causing them to idly wait for work. This
     * overrides the default policy of starting core threads only when
     * new tasks are executed.
     * 开启所有的核心线程,使它们处于空闲状态等待任务
     * 这将覆盖仅在执行新任务时启动核心线程的默认策略。
     * (因为tomcat需要启动完成后快速响应请求,起到了预热线程池的作用)
     * @return the number of threads started
     */
  public int prestartAllCoreThreads() {
        int n = 0;
        while (addWorker(null, true))
            ++n;
        return n;
    }

3.重写了父类的execute方法,进行扩展(重点)

  public void execute(Runnable command, long timeout, TimeUnit unit) {
        //将自己的计数器叠加
        submittedCount.incrementAndGet();
        try {
            //调用父类的模板流程
            super.execute(command);
        //这里catch住了RejectedExecutionException.正常JDK的规则是core线程数+临时线程数 > maxSize的时候,就抛出RejectedExecutionException。
        //这里catch住的话,继续往taskQueue里头放。
        } catch (RejectedExecutionException rx) {
            
            if (super.getQueue() instanceof TaskQueue) {
                final TaskQueue queue = (TaskQueue)super.getQueue();
                try {
                    if (!queue.force(command, timeout, unit)) {
                        submittedCount.decrementAndGet();
                        throw new RejectedExecutionException("Queue capacity is full.");
                    }
                } catch (InterruptedException x) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(x);
                }
            } else {
                submittedCount.decrementAndGet();
                throw rx;
            }

        }
    }

execute任务核心还是交给Java原生线程池处理。这里主要增加一个重试策略,如果原生线程池执行拒绝策略的情况,抛出RejectedExecutionException异常。这里将会捕获,然后重新再次尝试将任务加入到TaskQueue,尽最大可能执行任务。

tomcat的线程池使用了自己扩展的taskQueue,继承了LinkedBlockingQueue。
因为上文中提到LinkedBlockingQueue队列的默认长度为Integer.MAX_VALUE。所以默认创建的该队列有容量危险。
这一步修改使得,当达到最大线程数时,仍然尝试向TaskQueue队列添加任务。

参考资料

Java线程池实现原理及其在美团业务中的实践
线程池ThreadPoolExecutor---tomcat实现策略

posted @ 2021-03-29 08:47  cos晓风残月  阅读(312)  评论(0)    收藏  举报