juejin.cn

Tomcat的线程池Executor除了实现Lifecycle接口外,基本和JDK的ThreadPoolExecutor一致,以前是直接继承了JDK的ThreadPoolExecutor,并改写部分逻辑,在最新的代码上(Tomcat 10,2021.7.22以后),甚至是直接抄了一份,改写部分逻辑,然后再通过组合的方式使用。

主要区别是线程工厂、任务队列和拒绝策略上,先看看JDK线程池的执行策略,以及在这几个方面有什么缺陷。

JDK的线程池执行流程

首先需要知道线程池的几个基本概念:核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、阻塞任务队列、拒绝策略。JDK的线程池执行流程如下,当有新任务来临时:

文章内容收录到个人网站,方便阅读hardyfish.top/

首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

原生线程池在达到核心线程数时,是优先添加队列,这样比较适合CPU密集型任务(认为新建线程不如让任务排队)。

Tomcat面临的问题

但是Tomcat是属于IO密集型任务,在Tomcat看来,原生的线程池主要有两个问题:

1. 阻塞任务队列

JDK可选的几个阻塞任务队列无非是:LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue、LinkedTransferQueue。后两个显然不适合,候选只有前两个:链表和数组。

对于原生LinkedBlockingQueue,无界,那么线程池就不会满足上述第4步的条件,线程会在达到corePoolSize后不再新建,而是一直加入队列。而作为IO密集型的Tomcat,显然是希望此时创建新线程。

对于原生ArrayBlockingQueue,有界,但是线程池在第5步的时候,就会执行拒绝策略。

2. 拒绝策略

当满足第5步的时候,原生线程池就会执行拒绝策略,具体来说是j.u.c.RejectedExecutionHandler接口。JDK默认了四个实现:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}