为什么tomcat要自定义线程池实现?

背景

最近在研究tomcat调优的问题,开发人员做过的最多的tomcat调优想必就是线程池调优了,但是tomcat并没有使用jdk自己的线程池实现,而是自定了了线程池,自己实现了ThreadPoolExecutor类位于org.apache.tomcat.util.threads包下

jdk线程池

首先回顾一下jdk的线程池实现
为什么tomcat要自定义线程池实现?_第1张图片
提交一个任务时:
1 如果此时线程池中的数量小于corePoolSize,无论线程池中的线程是否处于空闲状态,都会创建新的线程来处理被添加的任务。

2 如果此时线程池中的数量大于等于于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。

3 如果此时线程池中的数量大于等于corePoolSize,且缓冲队列workQueue满,但是线程池中的数量小于maximumPoolSize,则建新线程来处理被添加的任务

4 如果此时线程池中的数量大于corePoolSize,且缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么就需要通过handler所指定的策略来处理此这个任务。

5 线程池中的线程数量大于 corePoolSize时,如果某线程处理完任务后进入空闲状态,空闲时间超过keepAliveTime,该线程将被终止。这样,线程池可以动态的调整池中的线程数到corePoolSize。

重点已经标红处理了:在线程达到corePoolSize个数时,超过的任务是先放在队列里面的

问题:当任务很多又很耗时(比如http请求IO密集型),队列长度怎么设置?过长容易造成任务堆积甚至OOM,最大线程数的设置也将变的没有意义;过短又容易将丢弃任务,tomcat至少要保证请求尽可能交给业务系统去处理

tomcat线程池

在AbstractEndpoint中调用createExecutor方法自定义线程池

public void createExecutor() {
        internalExecutor = true;
        TaskQueue taskqueue = new TaskQueue();
        TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
        executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
        taskqueue.setParent( (ThreadPoolExecutor) executor);
    }

那么首先看ThreadPoolExecutor的excute方法

public void execute(Runnable command, long timeout, TimeUnit unit) {
        submittedCount.incrementAndGet();
        try {
            super.execute(command);
        } 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(sm.getString("threadPoolExecutor.queueFull"));
                    }
                } catch (InterruptedException x) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(x);
                }
            } else {
                submittedCount.decrementAndGet();
                throw rx;
            }

        }
    }

tomcat中的线程池有一个额外的属性submittedCount,你可以简单的理解为就是一个计数器,每当有一个任务执行在执行时submittedCount就会加1,当任务执行完成后submittedCount就会减1,具体记录这个数字有什么作用,继续往下看就知道了。

核心还在是父类的的excute方法

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.
         *
         * 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.
         *
         * 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.
         */
        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);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
  1. 如果当前worker的数量小于核心线程池,调用addWorker
  2. 如果任务能成功入队,并且需要增加一个线程(线程池未关闭并且没有工作线程),调用addWorker
  3. 如果任务入队失败,那么尝试添加一个新线程。如果失败了,拒绝这项任务。

什么是ctl?

我们看到线程池ThreadPoolExecutor内部是通过AtomicInteger类型的 ctl变量来控制运行状态和线程个数;在通过AtomicInteger源码知道器内部就是维护了一个int类型的value值,如下

private volatile int value;

那么通过一个值如何维护状态和个数两个值呢?那么接下来通过底层源码来看大神Doug Lea是如何设计的。


    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;
 
/**
 * 各个值的二进制表示:
 *
 * 1111 1111 1111 1111 1111 1111 1111 1111 (-1) 
 * 0000 0000 0000 0000 0000 0000 0000 0000 (0) 
 * 0000 0000 0000 0000 0000 0000 0000 0001 (1) 
 * 0000 0000 0000 0000 0000 0000 0000 0010 (2) 
 * 0000 0000 0000 0000 0000 0000 0000 0011 (3)
 *
 * 【分析】:
 * 初始容量值,高三位全是0,低29位全是1;后续操作会以此为基础进行操作
 * CAPACITY                    000 1   1111 1111 1111 1111 1111 1111 1111
 *
 *              ---------------3位-1位 -28位---
 * 【前三位,表明状态位,后29位表明线程个数,即 2^29 - 1 基本够用了】
 *
 * RUNNING(-536870912)         111 0    0000 0000 0000 0000 0000 0000 0000
 * SHUTDOWN(0)                 000 0    0000 0000 0000 0000 0000 0000 0000
 * STOP(536870912)             001 0    0000 0000 0000 0000 0000 0000 0000
 * TIDYING(1073741824)         010 0    0000 0000 0000 0000 0000 0000 0000
 * TERMINATED(1610612736)      011 0    0000 0000 0000 0000 0000 0000 0000
 * 
 */


为什么tomcat要自定义线程池实现?_第2张图片

结论就是:前三位,表明状态位,后29位表明线程个数,即 2^29 - 1 基本够用了

TaskQueue的offer()方法

既然tomcat用了自己的队列,接下来看一下自定义的TaskQueue类

上述源码已经知道,是否调用addWorker方法取决于TaskQueue的offer()方法要返回false结果(暂不考虑入队成功但是worker随后被销毁的情况)

LinkedBlockingQueue的实现

TaskQueue继承于LinkedBlockingQueue,那么先看LinkedBlockingQueue的offer实现

public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)
            return false;
        final int c;
        final Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() == capacity)
                return false;
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }

很简单:队列中元素个数达到容量上限,则返回false
这也能解释为什么jdk线程池是队列满了才会继续新增线程至最大线程数

tomcat实现TaskQueue:

首先TaskQueue是继承LinkedBlockingQueue的

public class TaskQueue extends LinkedBlockingQueue<Runnable> 

再看offer方法

public boolean offer(Runnable o) {
      //we can't do any checks
        if (parent==null) return super.offer(o);
        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }

注释解释的很明显了:

  1. 如果当前活跃的线程数等于最大线程数,那么就不能创建线程了,因此直接放入队列中
  2. 如果当前提交的任务数小于等于当前活跃的线程数,表示还有空闲线程,直接添加到队列,让线程去执行即可。此处也终于看到了parent.getSubmittedCount方法,用来获取当前提交的任务数,每当有一个任务执行在执行时submittedCount就会加1,当任务执行完成后submittedCount就会减1
  3. 再校验下当前活跃线程数是否小于最大线程数,如果小于,此时就可以创建新的线程了。
  4. 如果以上都不符合,那就代表既没有空闲线程,又达到了最大线程数,也只能放队列了,但是不需要新建线程

为什么tomcat要自定义线程池实现?_第3张图片

因此Tomcat的线程池策略是,如果没有空闲线程且线程数大于核心线程配置时:继续增加线程至最大核心数为止

两者对比

tomcat的线程池实现相比jdk实现有以下几点不同:

  1. 队列无限长:高qps时基本不会丢弃任务;但是会有OOM的风险,但是一般单台服务器qps基本不会超过两千
  2. 达到最大线程数之后才会放入队列,低qps时可以快速请求响应不用排队,但是若qps不稳定,会频繁创建销毁线程,对cpu不够友好

行文至此,tomcat为什么要自定义线程池已经讲完了,接下来会补充一下worker的源码分析

worker原理

简介

Worker是ThreadPoolExecutor中的内部类,线程池中的线程,都会被封装成一个Worker类对象,ThreadPoolExecutor维护的其实就是一组Worker对象,其中用集合workers存储这些Worker对象;

Worker类中有两个属性,一个是firstTask,用来保存传入线程池中的任务,一个是thread,是在构造Worker对象的时候,利用ThreadFactory来创建的线程,用来处理线程池队列中的任务;

Worker继承AQS,使用AQS实现独占锁,并且是不可重入的,构造Worker对象的时候,会把锁资源状态设置成-1,因为新增的线程,还没有处理过任务,是不允许被中断的

代码如下

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;
	
	/** 这个就是worker持有的线程,也就是线程池中的线程 */    
	final Thread thread;
	
	/** 这个就是提交给线程池的任务 */    
	Runnable firstTask;
	
	/** 每一个线程执行的任务数量的计数器 */  
	volatile long completedTasks;
	
	/**
	 * 我们在调用addWorker方法的时候就会调用这个构造方法,有可能是创建新线程并执行任务,那么firstTask就是传给线程池要执行的任务,如果只是了
	 * 单纯的想创建一个线程,只需要传入null就可以
	 */
	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);
	}
	

}


worker其实就是一个Runable,其也是需要构造成一个Thread对象,然后调用Thread.start()方法运行的。只不过在worker的run方法中是定义了一个runWoker的方法。这个方法的主要内容从 for 循环不停的从task队列中获取对应的runable的task,然后同步调用这个task的run()方法。其实就是在某个线程中,不停的拿队列中的任务进行执行。

runWorker

上文已经知道线程池添加一个线程是通过调用addWorker方法,在调用addWorker成功后则会执行Worker对象的run方法,进入runWorker方法逻辑

final void runWorker(ThreadPoolExecutor.Worker w) {
    // 获取当前线程,其实这个当前线程,就是worker对象持有的线程,从线程池中拿到的任务就是由这个线程执行的
    Thread wt = Thread.currentThread();
    // 在构造Worker对象的时候,会把一个任务添加进Worker对象
    // 因此需要把其作为新增线程的第一个任务来执行
    Runnable task = w.firstTask;
    // 上面已经将该任务拿出来准备进行执行了(将firstTask取出赋值给task),则需要将该worker对象即线程池中的线程对象持有的任务清空
    w.firstTask = null;
    // 将AQS锁资源的状态由-1变成0,运行该线程进行中断 因为在创建的时候将state设为-1了,现在开始执行任务了,也就需要加锁了,所以要把state再重新变为0,这样在后面执行任务的时候才能用来加锁,保证任务在执行过程中不会出现并发异常
    // 解锁
    w.unlock();
    // 用来判断执行任务的过程中,是否出现了异常
    boolean completedAbruptly = true;
    try {
        // 线程池中的线程循环处理线程池中的任务,直到线程池中的所有任务都被处理完后则跳出循环
        while (task != null || (task = getTask()) != null) {  // 这一步的getTask()就说明Worker一直在轮询的从队列中获取任务,getTask()方法将从队列获取到的任务返回,赋值给task
            // 给该worker加锁,一个线程只处理一个任务。注意加锁是给worker线程加锁,不是给任务线程加锁,因为worker线程之前一直在轮询地在队列中取任务,但是当执行任务的时候,为了避免执行任务出现异常,就对其加锁
            w.lock();
            // 线程池是否是STOP状态
            // 如果是,则确保当前线程是中断状态
            // 如果不是,则确保当前线程不是中断状态
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                            runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                // 注意这里中断的是当前线程,也就是worker对象持有的线程
                wt.interrupt();
            
            try {
                // 扩展使用,在执行任务的run方法之前执行
                beforeExecute(wt, task);
                // 记录执行任务过程中,出现的异常
                Throwable thrown = null;
                try {
                    // 执行任务的run方法   当前线程环境就是worker对象持有的线程,所以本质就是woker对象在执行task任务的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 {
                    // 扩展使用,在执行任务的run方法之后执行
                    afterExecute(task, thrown);
                }
            } finally {
                // 执行完任务后,就将任务对象清空
                task = null;
                w.completedTasks++; // 该worker已经完成的任务数+1
                w.unlock();  // 将worker线程地锁释放
            }
        }
        // 正常执行完任务
        completedAbruptly = false;
    } finally {
        // 线程池中所有的任务都处理完后,或者执行任务的过程中出现了异常,就会执行该方法
        processWorkerExit(w, completedAbruptly);
    }
}

这个方法主要做几件事

  1. 如果 task 不为空,则开始执行 task
  2. 如果 task 为空,则通过 getTask()再去取任务,并赋值给 task,如果取到的 Runnable 不为空,则执行该任务
  3. 执行完毕后,通过 while 循环继续 getTask()取任务
  4. 如果 getTask()取到的任务依然是空,那么整个 runWorker()方法执行完毕

这个方法比较简单,如果忽略状态检测和锁的内容,本质就是如果有第一个任务,就先执行之,之后再从任务队列中取任务来执行,获取任务是通过getTask()来进行的。

到这里也就明白了线程池是怎么复用有限的线程数来执行大量的任务
那么worker是如何获取任务的呢?

核心方法 getTask()

这个方法用来向队列中轮询地尝试获取任务。该方法也是ThreadPoolExecutor中的方法。

这里重要的地方是第二个 if 判断,目的是控制线程池的有效线程数量。

由上文中的分析可以知道,在执行 execute 方法时,如果当前线程池的线程数量超过了 corePoolSize 且小于maximumPoolSize,并且 workQueue 已满时,则可以增加工作线程,但这时如果超时没有获取到任务,也就是 timedOut 为 true 的情况,说明 workQueue 已经为空了,也就说明了当前线程池中不需要那么多线程来执行任务了,可以把多于 corePoolSize 数量的线程销毁掉,保持线程数量在 corePoolSize 即可。

// 返回任务Runnable
private Runnable getTask() {
    // timedOut表示 记录上一次从队列中获取任务是否超时
    boolean timedOut = false; // Did the last poll() time out?
    // 自旋
    for (;;) {
        // 这一部分是判断线程池状态
        // 获取线程池的状态和线程池中线程数量组成的整形字段,32位
        // 高3位代表线程池的状态,低29位代表线程池中线程的数量
        int c = ctl.get();
        // 获取高3位的值,即线程池的状态
        int rs = runStateOf(c);
        // 如果线程池状态不是Running状态,并且 线程也不是SHUTDOWN状态 或者任务队列已空
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            // 则将线程池中的线程数量减1  就是说该线程已经不是运行状态了,所以要这个worker线程也没有用了,直接将该worker去掉。这个是原子操作
            decrementWorkerCount();
            //返回一个空任务,因为:
            // 1:如果任务队列已空,则想返回任务也没有
            // 2:如果线程池处于STOP或者之上的状态,则线程池不允许再处理任务
            return null;
        }
        // 这一部分是判断线程池有效线程数量
        // 获取低29位的值,即线程池中线程的数量
        int wc = workerCountOf(c);
        // timed是否需要进行超时控制
        // allowCoreThreadTimeOut默认false
        // 当线程池中线程的数量没有达到核心线程数量时,获取任务的时候允许超时  如果将allowCoreThreadTimeOut设为true,那也不允许超时
        // 当线程池中线程的数量超过核心线程数量时,获取任务的时候不允许超时   
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 这个很好理解
 
        // wc > maximumPoolSize的情况是因为可能在此方法执行阶段同时执行了setMaximumPoolSize方法;
        // timed && timedOut 如果为true,表示当前操作需要进行超时控制,并且上次从阻塞队列中获取任务发生了超时
        // 接下来判断,如果有效线程数量大于1,或者阻塞队列是空的,那么尝试将workerCount减1;
        // 如果减1失败,则continue返回重试
        // 如果wc == 1时,也就说明当前线程是线程池中唯一的一个线程了。
        if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
 
        // 如果上面都没问题,就可以获取任务了
        try {
            // 获取任务
            // 如果timed = true ,说明需要做超时控制,则根据keepAliveTime设置的时间内,阻塞等待从队列中获取任务
            // 如果timed = false,说明不需要做超时控制,则阻塞,直到从队列中获取到任务为止
            Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
            // 如果获取到任务,则把任务返回
            if (r != null)
                return r;
            // 执行到这里,说明在允许的时间内,没有获取到任务
            timedOut = true;
        } catch (InterruptedException retry) {
            // 获取任务没有超时,但是出现异常了,将timedOut设置为false
            timedOut = false;
        }
    }
}


注意,这里取任务会根据工作线程的数量判断是使用BlockingQueue的poll(timeout, unit)方法还是take()方法。

poll(timeout, unit)方法会在超时时返回null,如果timeout<=0,队列为空时直接返回null。

take()方法会一直阻塞直到取到任务或抛出中断异常。

所以,如果keepAliveTime设置为0,当任务队列为空时,非核心线程取不出来任务,会立即结束其生命周期。

当允许超时控制时:workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 会在keepAliveTime后返回null,返回null的结果是退出完成循环,销毁当前线程;这就说明了keepAliveTime参数的含义,线程最大空闲时间了

提出一个问题,这个keepAliveTime参数是否对核心线程也生效呢,文末会有答案

那么接下来,worker是如何销毁的呢?

核心方法 processWorkerExit()

runWorker 的 while 循环执行完毕以后,在 finally 中会调用 processWorkerExit()方法,来销毁工作线程。该方法就是判断当前线程是需要将其删除还是继续执行任务。该方法也是ThreadPoolExecutor中的方法。

private void processWorkerExit(ThreadPoolExecutor.Worker w, boolean completedAbruptly) {
    // 如果 completedAbruptly = true ,则线程执行任务的时候出现了异常,需要从线程池中减少一个线程
    // 如果 completedAbruptly = false,则执行getTask方法的时候已经减1,这里无需在进行减1操作
    if (completedAbruptly)
        decrementWorkerCount();
    
    // 获取线程池的锁,因为后面是线程池的操作,为了并发安全,需要对线程池加锁
    final ReentrantLock mainLock = this.mainLock;
    // 线程池加锁
    mainLock.lock();
    try {
        // 统计该线程池完成的任务数
        completedTaskCount += w.completedTasks;
        // 从线程池中移除一个工作线程    works是线程池持有的一个集合  
        workers.remove(w); // 将没用的worker去掉,也就是当前传入的worker
    } finally {
        // 线程池解锁
        mainLock.unlock();
    }
    // 根据线程池的状态,决定是否结束该线程池
    tryTerminate(); // 钩子方法
 
    // 判断线程池是否需要增加线程
    // 获取线程池的状态
    int c = ctl.get();
    // -当线程池是RUNNING或SHUTDOWN状态时
    // --如果worker是异常结束(即completedAbruptly为false),那么会直接addWorker;
    // ---如果allowCoreThreadTimeOut = true,并且等待队列有任务,至少保留一个worker;
    // ---如果allowCoreThreadTimeOut = false,活跃线程数不少于corePoolSize
    if (runStateLessThan(c, STOP)) { // 线程池状态小于STOP,就说明当前线程池是RUNNING或SHUTDOWN状态
        // 如果worker是异常结束的,不进入下面的分支,直接去addWorker
        if (!completedAbruptly) {
            // 根据allowCoreThreadTimeOut的值,来设置线程池中最少的活跃线程数是0还是corePoolSize
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            // 如果等待队列中有任务,要至少保留一个worker
            if (min == 0 && ! workQueue.isEmpty())
                // 至少保留一个worker
                min = 1;
            // 如果活跃线程数大于等于min,直接返回,不需要再调用addWorker来增加线程池中的线程了
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        // 增加线程池中的worker
        addWorker(null, false);
    }
}


1.执行decrementWorkerCount方法将线程池中的线程数量减1 ,因为当前worker已经取出去任务了

  1. 如果 completedAbruptly = true ,则代表线程执行任务的时候出现了异常,需要执行
  2. 如果 completedAbruptly = false,则执行getTask方法的时候调用过decrementWorkerCount方法将线程池中的线程数量减1,这里无需在进行减1操作

2.将worker从wokers集合中移除
3.根据线程池的状态,决定是否结束该线程池
4.判断是否再调用addWorker方法

其中 int min = allowCoreThreadTimeOut ? 0 : corePoolSize;根据allowCoreThreadTimeOut的值,来设置线程池中最少的活跃线程数是0还是corePoolSize

  1. 如果worker是异常结束(即completedAbruptly为false),那么会直接addWorker;
  2. 如果allowCoreThreadTimeOut = true,并且等待队列有任务,至少保留一个worker;
  3. 如果allowCoreThreadTimeOut = false,活跃线程数不少于corePoolSize

说明

  1. 如果核心线程出了异常也是会被销毁的,只不过销毁后还会调用addWorker方法增加一个线程
  2. allowCoreThreadTimeOut为true时,min为0,则表明,核心线程在allowCoreThreadTimeOut为true时也是会随着worker的销毁(没有任务可取既空闲了keepAliveTime时间)而销毁,并且不会调用addWorker来增加一个线程

参考:
https://blog.csdn.net/cy973071263/article/details/131484384
https://blog.csdn.net/u014307520/article/details/117787133
https://blog.csdn.net/kkkkk0826/article/details/103405813

你可能感兴趣的:(Java基础,多线程,调优,tomcat,java)