Tomcat 线程池原理分析

前言

 
通常我们可以将执行的任务分为两类
 
1)CPU 密集型任务
需要线程长时间进行的复杂的运算,这种类型的任务需要少创建线程,过多的线程将会频繁引起上文切换,降低任务处理处理速度
 
2)IO 密集型任务
由于线程并不是一直在运行,可能大部分时间在等待 IO 读取/写入数据,增加线程数量可以提高并发度,尽可能多处理任务
 
对于 JDK 自带的 ThreadPoolExecutor ,逻辑是先创建核心线程数,如果满了,则加到队列中,等待后续被执行;如果队列满了,且 worker 数不超过最大线程数的情况下,会新开线程去执行任务。这种设计适用于 CPU 密集型
 
Tomcat 的场景是不停的接收请求,这过程涉及到网络 IO,同时需要支持较高的并发,如果核心线程满了,就将请求放到队列中等待后续执行,那么这个请求从发起到结束可能会持续很久,这对于 web 服务器来说是无法接受的,所以 JDK 的 ThreadPoolExecutor 并不适用于 Tomcat
 
 

自定义 ThreadPoolExecutor

 
为了解决上面的问题,Tomcat 采用重写 JDK ThreadPoolExecutor 的方式,在 Tomcat 中就有这么一个叫 ThreadPoolExecutor 的类,代码照搬的 JDK 中的 ThreadPoolExecutor,同时在此基础上做了部分修改
 
下面我们从 Tomcat 对 ThreadPoolExecutor 的修改入手,简单看看 Tomcat 的设计是怎样的。这里用的 Tomcat 版本为 9.0.56,在看下文之前,建议先看下 JDK 中 ThreadPoolExecutor 的实现,对接下来的分析会很有帮助
 

createExecutor

 
首先是创建线程池的代码,位于 AbstractEndpoint 类中
 
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);
} 

 

上面的 TaskQueue ,TaskThreadFactory,ThreadPoolExecutor 都是 Tomcat 中的类
 
TaskQueue 继承了 JDK 中的 LinkedBlockingQueue,默认是一个无边界队列
 
public TaskQueue() {
    super();
}

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
 
TaskThreadFactory 很简单,没什么好说的,再来看 ThreadPoolExecutor
 

execute

 
首先是 ThreadPoolExecutor 中的 execute 方法,用于执行请求任务,代码如下
 
public void execute(Runnable command, long timeout, TimeUnit unit) {
    submittedCount.incrementAndGet();
    try {
        executeInternal(command);
    } catch (RejectedExecutionException rx) {
        if (getQueue() instanceof TaskQueue) {
            // If the Executor is close to maximum pool size, concurrent
            // calls to execute() may result (due to Tomcat's use of
            // TaskQueue) in some tasks being rejected rather than queued.
            // If this happens, add them to the queue.
            final TaskQueue queue = (TaskQueue) 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;
        }
    }
}
 
相较于 JDK 自带的 ThreadPoolExecutor ,上面多了 submittedCount.incrementAndGet() 和 catch RejectedExecutionException 之后的那部分代码
 
先说 submittedCount,是一个 AtomicInteger ,作用是统计队列中的任务数和在执行中但还未完成的任务数之和
 
/**
 * The number of tasks submitted but not yet finished. This includes tasks
 * in the queue and tasks that have been handed to a worker thread but the
 * latter did not start executing the task yet.
 * This number is always greater or equal to {@link #getActiveCount()}.
 */
private final AtomicInteger submittedCount = new AtomicInteger(0);
 
catch 中的代码很好理解,作用是让被拒绝的请求再次加入到队列中,避免出现请求遗漏
 

executeInternal

 
然后再来看 executeInternal 方法,代码和逻辑如下
 
1)如果当前 work 数小于核心线程数,则创建 work,然后返回
 
2)如果条件一不满足且线程池还行运行中,则将请求入队
 
3)如果条件二不满足,则直接创建新的 work 去执行请求
 
private void executeInternal(Runnable command) {
    if (command == null) {
        throw new NullPointerException();
    }
    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);
    }
}

 

offer

 
上面重点需要看的是 workQueue.offer(command),代码和逻辑如下
 
1)如果 parent 为 null,入队,实际上这个 parent 就是 ThreadPoolExecutor,在最开始创建线程池的时候有一句 taskqueue.setParent( (ThreadPoolExecutor) executor)
 
2)parent.getPoolSize() 的作用是返回当前线程池中的线程数,如果等于最大线程数,就将请求入队,等待后续被执行
 
3)如果提交数小于等于当前线程数,表示有空闲着的线程,将请求入队,让空闲着的线程竞争去执行这个请求
 
4)当上面三个条件都没有满足,表示当前线程池中的线程数小于最大线程数,并且没有空闲线程去执行新进来的请求,这里直接返回 false,这样的话就会执行 executeInternal 的 addWorker 方法,开一个非核心线程去执行进来的请求
 
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)通过 submittedCount 来记录提交但未完成的请求数,当线程池中的线程大于等于该值时,表示有空闲着的线程,就会将请求入队
 
2)如果线程池中没有空闲线程,就会新开非核心线程去处理进来的请求
 
3)非核心线程在处理完队列中的请求后会被销毁,减少线程资源的占用,这一点和 JDK 的 ThreadPoolExecutor 是一样的
 

posted @ 2022-01-27 14:35  happyhbao  阅读(860)  评论(1)    收藏  举报