Tomcat的线程池Executor除了实现Lifecycle接口外,基本和JDK的ThreadPoolExecutor一致,以前是直接继承了JDK的ThreadPoolExecutor,并改写部分逻辑,在最新的代码上(Tomcat 10,2021.7.22以后),甚至是直接抄了一份,改写部分逻辑,然后再通过组合的方式使用。
主要区别是线程工厂、任务队列和拒绝策略上,先看看JDK线程池的执行策略,以及在这几个方面有什么缺陷。
首先需要知道线程池的几个基本概念:核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、阻塞任务队列、拒绝策略。JDK的线程池执行流程如下,当有新任务来临时:
文章内容收录到个人网站,方便阅读:hardyfish.top/
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
原生线程池在达到核心线程数时,是优先添加队列,这样比较适合CPU密集型任务(认为新建线程不如让任务排队)。
但是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);
}