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 是一样的

浙公网安备 33010602011771号