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

news2025/5/13 5:40:21

背景

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

jdk线程池

首先回顾一下jdk的线程池实现
在这里插入图片描述
提交一个任务时:
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
 * 
 */


在这里插入图片描述

结论就是:前三位,表明状态位,后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的线程池策略是,如果没有空闲线程且线程数大于核心线程配置时:继续增加线程至最大核心数为止

两者对比

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1029491.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据湖存储在大模型中的应用

9月5日&#xff0c;浪潮信息新产品“互联网AIGC”行业巡展在深圳举行。本次巡展以“智算 开新局创新机”为主题&#xff0c;腾讯云存储受邀分享数据湖存储在大模型中的应用&#xff0c;并在展区对腾讯云存储解决方案进行了全面的展示&#xff0c;引来众多参会者围观。 ChatGPT…

A股20年数据回测结果mysql数据查询 phpadmin

编辑 数据库登录使用 1.登录mysql数据库管理台 phpadmin 访问地址&#xff1a; http://121.43.55.160:888/phpmyadmin_c77c64465f15a891/index.php 用户名&#xff1a;root 密码&#xff1a; root 2.切换到阿里云服务器 3 数据库密码 用户名&#xff1a;readonly 密码&am…

堆的OJ题

&#x1f525;&#x1f525; 欢迎来到小林的博客&#xff01;&#xff01;       &#x1f6f0;️博客主页&#xff1a;✈️林 子       &#x1f6f0;️博客专栏&#xff1a;✈️ 小林的算法笔记       &#x1f6f0;️社区 :✈️ 进步学堂       &am…

【数据结构】二叉树的·深度优先遍历(前中后序遍历)and·广度优先(层序遍历)

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

2023最新Nmap安装和使用详解,超详细教程

【点击文章末尾卡片&#xff0c;先领资料再阅读哦~】&#x1f447; 文章目录 【点击文章末尾卡片&#xff0c;先领资料再阅读哦~】&#x1f447; Nmap概述功能概述运行方式 Nmap安装Nmap参数详解目标说明主机发现端口扫描端口说明和扫描顺序服务与版本探测脚本扫描操作系统探测…

C++输入流和输出流介绍

C 又可以称为“带类的 C”&#xff0c;即可以理解为 C 是 C 语言的基础上增加了面向对象&#xff08;类和对象&#xff09;。在此基础上&#xff0c;学过 C 语言的读者应该知道&#xff0c;它有一整套完成数据读写&#xff08;I/O&#xff09;的解决方案&#xff1a; 使用 scan…

DAZ To UMA⭐二.设置DAZ导出的变形内容 和 获取模型纹理贴图位置

文章目录 🟩 设置DAZ导出的内容1️⃣ 找到要导出的参数名称2️⃣ 打开导出面板3️⃣ 设置导出规则举例 : 导出身体Assets🟦 获取模型纹理贴图🟩 设置DAZ导出的内容 设置参数有两个目的: DAZ可以进行模型的参数调整,例如胖瘦等等,那如何将这些调整后的数值一起导出到FBX中…

Window通过VMWare搭建Linux集群后,将虚拟机暴露到宿主机局域网中,实现个人服务器搭建

目录 一、目的二、实现的方法 2.1 保证VMnet8为NAT模式2.2 获取虚拟机IP后&#xff0c;与宿主机进行端口绑定2.3 查看宿主机端口配置是否生效2.4 宿主机的端口准入规则设置2.5 效果 一、目的 解决宿主机通过VMware WorkStation安装了虚拟机后&#xff0c;宿主机所在的局域网…

A股风格因子看板 (2023.09 第07期)

该因子看板跟踪A股风格因子&#xff0c;该因子主要解释沪深两市的市场收益、刻画市场风格趋势的系列风格因子&#xff0c;用以分析市场风格切换、组合风格景露等。 今日为该因子跟踪第7期&#xff0c;指数组合数据截止日2023-08-31&#xff0c;要点如下 近1年A股风格因子收益走…

开源日报 0821:帮你修复老旧照片

这篇文章总结了几个开源项目的特点和优势。其中包括了 Python 资源列表、金融研究工具、动画精灵程序、游戏和旧照片修复项目等。这些项目提供了丰富的功能和技术支持&#xff0c;用户可以根据自己的需求进行定制和改进。总的来说&#xff0c;这些开源项目为开发者和用户提供了…

在如今内卷严重的背景下,程序员该如何提高自己的收入,享受美好人生?

无论是当今程序员行业&#xff0c;还是各行各业&#xff0c;内卷都非常严重&#xff0c;好多人都处于了一种卷又卷不过&#xff0c;躺又躺不平的境地&#xff0c;十分的难受&#xff0c;那么作为一个程序员而言&#xff0c;该如何在这样的境地下提高自己的收入&#xff0c;享受…

动力节点老杜JavaWeb笔记(全)

Servlet 关于系统架构 系统架构包括什么形式&#xff1f; C/S架构B/S架构 C/S架构&#xff1f; Client / Server&#xff08;客户端 / 服务器&#xff09;C/S架构的软件或者说系统有哪些呢&#xff1f; QQ&#xff08;先去腾讯官网下载一个QQ软件&#xff0c;几十MB&#xff…

Innodb底层原理与Mysql日志机制

MySQL内部组件结构 Server层 主要包括连接器、词法分析器、优化器、执行器等&#xff0c;涵盖 MySQL 的大多数核心服务功能&#xff0c;以及所有的内置函数&#xff08;如日期、时间、数学和加密函数等&#xff09;&#xff0c;所有跨存储引擎的功能都在这一层实现&#xff0c…

1536. 排布二进制网格的最少交换次数;754. 到达终点数字;1106. 解析布尔表达式

1536. 排布二进制网格的最少交换次数 核心思想&#xff1a;贪心。枚举每一行&#xff0c;然后去找离这行最近的满足要求的那一行。我们用j保存每一行的最后一个1&#xff0c;那么尾部的0个数为n-1-j&#xff0c;每一行需要的0个数为n-i-1&#xff0c;所以得出结论 i > j。然…

asp.net网站的建立及运行

点击创建新项目 在输入框中输入asp.net&#xff0c;并选择图中的 点击下一步 点击创建 然后&#xff0c;右键&#xff0c;添加&#xff0c;新建项 选择web窗体 点击添加 点击视图&#xff0c;工具箱 选择一个label&#xff0c;记住这个id 空白处右键&#xff0c;查看代码 添…

usmile F10 系列数字牙刷:数字化刷牙体验的新巅峰

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页: &#x1f405;&#x1f43e;猫头虎的博客&#x1f390;《面试题大全专栏》 &#x1f995; 文章图文并茂&#x1f996…

51单片机4【玩转开发板】

1.开发板完全介绍 1.配件 2.开发板主板及板载原件 1.单片机 &#xff08;1&#xff09;注意不要装反了&#xff0c;凹槽的一面向上是正的 &#xff08;2&#xff09;不要再开电的时候取下或者装上。 2.单片机工作必备的外围电路 1.晶振 2.复位电路 3.电源供电电路 4.USB下载…

NLP文本生成全解析:从传统方法到预训练完整介绍

目录 1. 引言1.1 文本生成的定义和作用1.2 自然语言处理技术在文本生成领域的使用 2 传统方法 - 基于统计的方法2.1.1 N-gram模型2.1.2 平滑技术 3. 传统方法 - 基于模板的生成3.1 定义与特点3.2 动态模板 4. 神经网络方法 - 长短时记忆网络(LSTM)LSTM的核心概念PyTorch中的LST…

漫画 | 两年,我学会了所有的编程语言!

本文想要探讨的一个话题是&#xff1a;作为一个程序员&#xff0c;如何看待这么多的编程语言&#xff1f;是不是要学习&#xff1f;该怎么学习&#xff1f;其实很多人都有这样的体会&#xff0c;当你学会了一门语言以后&#xff0c;再去学习新的语言&#xff0c;难度会大大降低…

音乐随行,公网畅享,群辉Audiostation给你带来听歌新体验!

文章目录 本教程解决的问题是&#xff1a;按照本教程方法操作后&#xff0c;达到的效果是本教程使用环境&#xff1a;1 群晖系统安装audiostation套件2 下载移动端app3 内网穿透&#xff0c;映射至公网 很多老铁想在上班路上听点喜欢的歌或者相声解解闷儿&#xff0c;于是打开手…