130行
taskqueue.setParent(executor);
这行代码非常关键。没有这行代码,Tomcat 的线程池则会表现的和 JDK 的线程池一样。
拿下面的程序举例:
自定义线程池最多可以容纳 150+300 个任务。
当 24 行注释的时候,Tomcat 线程池运行的过程和 JDK 线程池的运行过程一样,运行的线程数只会是核心程序数 5。
当 24 行取消注释的时候,Tomcat 线程池就会一直创建线程个数到 150 个,然后把剩下的任务提交到自定义的 TaskQueue 队列里面去。
我再提供一个复制粘贴直接运行版本,你分别运行一下,试一试,看看结果:
public class TomcatThreadPoolExecutorTest { public static void main(String[] args) throws InterruptedException { String namePrefix = "why不止技术-exec-"; boolean daemon = true; TaskQueue taskqueue = new TaskQueue(300); TaskThreadFactory tf = new TaskThreadFactory(namePrefix, daemon, Thread.NORM_PRIORITY); ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 150, 60000, TimeUnit.MILLISECONDS, taskqueue, tf); //taskqueue.setParent(executor); for (int i = 0; i < 300; i++) { try { executor.execute(() -> { logStatus(executor, "创建任务"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } }); } catch (Exception e) { e.printStackTrace(); } } Thread.currentThread().join(); } private static void logStatus(ThreadPoolExecutor executor, String name) { TaskQueue queue = (TaskQueue) executor.getQueue(); System.out.println(Thread.currentThread().getName() + "-" + name + "-:" + "核心线程数:" + executor.getCorePoolSize() + "\t活动线程数:" + executor.getActiveCount() + "\t最大线程数:" + executor.getMaximumPoolSize() + "\t总任务数:" + executor.getTaskCount() + "\t当前排队线程数:" + queue.size() + "\t队列剩余大小:" + queue.remainingCapacity()); } }
接着就去分析这行代码的用途,看看这一行代码,怎么就反转了 JDK 线程池的运行过程。
源码之下无秘密
如果你对 JDK 线程池的源码熟悉一点的话,你大概能猜到 Tomcat 肯定是在控制新建线程的地方做了手脚,也就是下面这个地方:
PS:需要说明一下的是,上面的截图是 JDK 线程池的 execute 方法。因为 Tomcat 线程池的提交也是复用的这个方法。但是 workQueue 不是同一个队列。
那你先把工作流程和各个参数都摸熟了,然后写个 Demo ,接着去疯狂的 Debug 吧。然后你总会找到这个地方的,而且你会发现,不难找。
好了,上面主要关注我圈起来的部分。
在截图的 1371 行,如果没有把任务成功放到队列里面(前提是线程池是运行状态),则会执行 1378 行的逻辑,而这个逻辑,就是创建非核心线程的逻辑。
所以,经过上面的推导之后,一切都清晰了,Tomcat 只需要在自定义队列的 offer 方法中做文章即可。
所以,我们重点关注一下该方法:
org.apache.tomcat.util.threads.TaskQueue#offer
为了更加直观的看出来其运行流畅,我在第 80 行打了个断点运行程序如下:
可以看到里面的几个参数,下面的讲解会用到这里面的参数:
第一个 if 判断
首先第一个 if,判断 parent 是否为空:
从断点运行参数截图可以看出,这里的 parent 就是 Tomcat 的 ThreadPoolExecutor 类。
当 parent 为 null 时,直接调用原始的 offer 方法。
所以,还记得我前面说的吗?
现在你知道为什么了吧?
源码,就是这个源码。道理,就是这么个道理。
所以,这里不为空,不满足条件,进入下一个 if 判断。
第二个 if 判断
首先,需要明确的是,能进入到第二个判断的时候,当前运行中的线程数肯定是大于等于核心线程数(因为已经在执行往队列里面放的逻辑了,说明核心线程数肯定是满了),小于最大线程数的。
其中 getPoolSize 方法是获取线程池中当前运行的线程数量:
所以,第二个 if 判断的是运行中的线程数是否等于最大线程数。如果等于,说明所有线程都在工作了,把任务扔到队列里面去。
从断点运行参数截图可以看到, 当前运行数为 5 ,最大线程数为 150。不满足条件,进入下一个 if 判断。
第三个 if 判断
首先我们看看 getSubmittedCount 获取的是个什么玩意:
getSubmittedCount 获取的是当前已经提交但是还未完成的任务的数量,其值是队列中的数量加上正在运行的任务的数量。
从断点运行参数截图可以看到,当前情况下该数据为 6。
而 parent.getPoolSize() 为 5。
不满足条件,进入下一个 if 判断。
但是这个地方需要多说一句的是,如果当已经提交但是还未完成的任务的数量小于线程池中运行线程的数量时,Tomcat 的做法是把任务放到队列里面去,而不是立即执行。
其实这样想来也是很符合逻辑且简单的做法的。
反正有空闲的线程嘛,扔到队列里面去就被空闲的线程消费了。又何必立即执行呢?破坏流程不说,还需要额外实现。
出力不讨好。没必要。
第四个 if 判断
这个判断就很关键了。
如果当前运行中的线程数量小于最大线程数,返回 false。
注意哦,前面的几个 if 判断都是不满足条件就放入队列哦。而这里是不满足条件,就返回 false。
返回 false 意味着什么?
意味着要执行 1378 行代码,去创建线程了呀。
所以,整个流程图大概就是这样: