那些有趣的代码(一)--有点萌的 Tomcat 的线程池
最近抓紧时间看看了看tomcat 和 jetty 的源代码。发现了一些有趣的代码,这里和大家分享一下。
Tomcat 作为一个老牌的 servlet 容器,处理多线程肯定得心应手,为了能保证多线程环境下的高效,必然使用了线程池。
但是,Tomcat 并没有直接使用 j.u.c 里面的线程池,而是对线程池进行了扩展,首先我们回忆一下,j.u.c 中的线程池的几个核心参数是怎么配合的:
- 如果当前运行的线程,少于corePoolSize,则创建一个新的线程来执行任务。
- 如果运行的线程等于或多于 corePoolSize,将任务加入 BlockingQueue。
- 如果 BlockingQueue 内的任务超过上限,则创建新的线程来处理任务。
- 如果创建的线程超出 maximumPoolSize,任务将被拒绝策略拒绝。
这个时候我们来仔细看看 Tomcat 的代码:
首先写了一个 TaskQueue 继承了非阻塞无界队列 LinkedBlockingQueue<Runnable>
并重写了的 offer 方法:
1 |
|
在提交任务的时候,增加了几个分支判断。
首先我们看看 parent 是什么:
1 | private transient volatile ThreadPoolExecutor parent = null; |
这里需要特别注意这里的 ThreadPoolExecutor 并不是 jdk里面的 java.util.concurrent.ThreadPoolExecutor 而是 tomcat 自己实现的。
我们分别来看 offer 中的几个 if 分支。
首先我们需要明确一下,当一个线程池需要调用阻塞队列的 offer 的时候,说明线程池的核心线程数已经被占满了。(记住这个前提非常重要)
要理解下面的代码,首先需要复习一下线程池的 getPoolSize() 获取的是什么?我们看源码:
1 | /** |
需要注意的是,workers.size() 包含了 coreSize 的核心线程和临时创建的小于 maxSize 的临时线程。
先看第一个 if
1 | // 如果线程池的工作线程数等于 线程池的最大线程数,这个时候没有工作线程了,就尝试加入到阻塞队列中 |
经过第一个 if 之后,线程数必然在核心线程数和最大线程数之间。
1 | if (parent.getSubmittedCount()<=(parent.getPoolSize())) { |
对于 parent.getSubiitedCount() ,我们要先搞清楚 submiitedCount 是什么
1 | /** |
这个数是一个原子类的整数,用于记录提交到线程中,且还没有结束的任务数。包含了在阻塞队列中的任务数和正在被执行的任务数两部分之和 。
所以这行代码的策略是,如果已提交的线程数小于等于线程池中的线程数,表明这个时候还有空闲线程,直接加入阻塞队列中。为什么会有这种情况发生?其实我的理解是,之前创建的临时线程还没有被回收,这个时候直接把线程加入到队里里面,自然就会被空闲的临时线程消费掉了。
我们继续往下看:
1 | //if we have less threads than maximum force creation of a new thread |
由于上一个 if 条件的存在,走到这个 if 条件的时候,提交的线程数已经大于核心线程数了,且没有空闲线程,所以返回一个 false 标明,表示任务添加到阻塞队列失败。线程池就会认为阻塞队列已经无法继续添加任务到队列中了,根据默认线程池的工作逻辑,线程池就会创建新的线程直到最大线程数。
回忆一下 jdk 默认线程池的实现,如果阻塞队列是无界的,任务会无限的添加到无界的阻塞队列中,线程池就无法利用核心线程数和最大线程数之间的线程数了。
Tomcat 的实现就是为了,线程池即使核心线程数满了以后,且使用无界队列的时候,线程池依然有机会创建新的线程,直到达到线程池的最大线程数。
Tomcat 对线程池的优化并没结束,Tomcat 还重写了线程池的 execute 方法:
1 | public void execute(Runnable command, long timeout, TimeUnit unit) { |
终于到整篇文章的萌点了,就是提交线程的时候,如果被线程池拒绝了,Tomcat 的线程池,还会厚着脸皮再次尝试,调用 force() 方法”强行”的尝试向阻塞队列中添加任务。
在群里和朋友讲完 Tomcat 线程池的实现,帆哥给了一个特别厉害的例子。
总结一下:
Tomcat 线程池的逻辑:
- 如果当前运行的线程,少于corePoolSize,则创建一个新的线程来执行任务。
- 如果线程数大于 corePoolSize了,Tomcat 的线程不会直接把线程加入到无界的阻塞队列中,而是去判断,submittedCount(已经提交线程数)是否等于 maximumPoolSize。
- 如果等于,表示线程池已经满负荷运行,不能再创建线程了,直接把线程提交到队列,
- 如果不等于,则需要判断,是否有空闲线程可以消费。
- 如果有空闲线程则加入到阻塞队列中,等待空闲线程消费。
- 如果没有空闲线程,尝试创建新的线程。(这一步保证了使用无界队列,仍然可以利用线程的 maximumPoolSize)。
- 如果总线程数达到 maximumPoolSize,则继续尝试把线程加入 BlockingQueue 中。
- 如果 BlockingQueue 达到上限(假如设置了上限),被默认线程池启动拒绝策略,tomcat 线程池会 catch 住拒绝策略抛出的异常,再次把尝试任务加入中 BlockingQueue 中。
- 再次加入失败,启动拒绝策略。
如此努力的 Tomcat 线程池,有点萌啊。