Tomcat线程池
肖文英
1 池化技术
池化技术是一种常用的提升性能的手段,比如常见的数据库连接池、JAVA 字符串的常量池、以及线程池等。
以 JAVA 中的线程为例,创建一个新线程的背后,其实是调用操作系统的 api,消耗宝贵的系统资源,去执行一些业务逻辑。如果我们频繁的创建销毁线程对象,对于 CPU 和内存都是一个很大的负担。所以,线程池就应运而生了。本质上,池化技术是一种以空间换时间的做法。
线程池就是提前创建好一些线程供我们使用,使用完毕之后并不销毁这些线程,而是放回池中,等待下次继续使用。
而 tomcat 作为 web 界的容器一哥,自然也使用了线程池这种优化手段,为了提高业务处理能力和支持更高的并发,tomcat 还对线程池做了更进一步的优化。
2 原生线程池
讲到 tomcat 优化线程池之前,我们先回顾一下 JAVA 原生的线程池。
JAVA 原生线程池 ThreadPoolExecutor 位于 java.util.concurrent 包下。
可以看到,ThreadPoolExecutor 提供了 7 个参数。
其工作原理大致如下:
当线程池收到一个新任务,先判断当前线程数是否大于 corePoolSize,如果小于 corePoolSize,就新建一个线程执行任务。
当线程池中的线程数已经达到 corePoolSize,再来新的任务就不会新建线程了,而是将任务投递到 workQueue 中。当核心线程有空闲了,就会从 workQueue 队列 poll 任务去执行。
如果任务非常多,workQueue 已经达到最大任务数量了。这时 maximumPoolSize 就会发挥作用了。线程池会继续创建新线程用于执行任务,直至最大线程数达到 maximumPoolSize。
如果任务继续增加,所有线程都满负荷工作了,队列也满了。此时线程池就会开始执行拒绝策略了,就是我们定义的 handler。
当任务高峰期过去了,临时线程闲置下来了,此时线程池就会从 workQueue 中拉取任务继续执行了。临时线程使用 poll(keepAliveTime, unit)方法拉取任务,如果在指定的 unit 时间内未获取到任务,临时线程就会被销毁回收。
3 tomcat 的线程池
通过对 JAVA 原生线程池的介绍,我们可以知道,当达到最大线程数时,表示线程池已经满负荷运行,无法再接收任务了。此时会执行线程池的拒绝策略,比如抛出异常、丢弃任务等。
那 tomcat 做了什么优化手段来提升并发量呢?
其实原理很简单:当线程数达到最大线程 maximumPoolSize 时,不是立刻执行拒绝策略。而是先尝试将任务投递到任务队列中,如果任务队列此时仍然是满的,再执行拒绝策略。
如上所示,就是 tomcat 线程池的 execute 方法。
executeInternal 方法的代码和 JAVA 原生线程池的 execute 方法是一样的,tomcat 只是将原生执行方法包装了一层。
tomcat 在外层 catch 了 RejectedExecutionException 异常,当异常抛出时,表示任务已满需要执行拒绝策略了。此时 tomcat 尝试将任务再次投递到任务队列,如果投递失败,再抛出一次 RejectedExecutionException 异常,转而去执行拒绝策略。
看到这,有的朋友可能会问了:任务都满了,再投递一次任务到队列中有什么用呢?
3.1 再次投递作用
比如一个平稳运行的系统忽然遇到大量的流量涌入,但是这些请求可能大部分都是一些简单的 CPU 密集型任务,比如简单的计算、查询,并非耗时较长的 IO 任务。
一开始,tomcat 默认的 200 个线程和 10000 的任务队列可能瞬间就被打满了,下一个任务进来时,由于线程池已经满负荷运行,可能就需要执行拒绝策略。
但是这些简单的请求耗时是非常短的,可能几毫秒就已经任务完成。所以过了几毫秒后,任务队列已经有了可继续投递的剩余空间。
那么简单的再次投递任务队列的尝试,可能就会使本该抛异常的请求继续执行。那么反应到应用层面,就是服务器能继续正常处理请求,从而提升了服务器的并发处理能力。
3.2 重写 offer 方法
另外,大家可以在 execute 方法中看到第一行的:submittedCount.incrementAndGet();
默认情况下,tomcat 的任务队列 TaskQueue 的 capacity 是 Integer.MAX_VALUE。
这样的话,当线程数达到核心线程数以后,再来新的任务都会被投递到任务队列中,就没有办法再创建新线程了,这样肯定是不行的。
所以,tomcat 重写了 JDK 的 LinkedBlockingQueue 的 offer 方法。
3.3 记录提交的任务个数
总结一下就是,对于一个新任务,有空闲的线程就先用空闲的线程,线程不够用又没达到上限就去创建新线程,线程达到上限就扔到队列排队去,队列满了执行重试机制,重试失败那就没办法了,执行拒绝策略吧。
4 推荐配置
tomcat 的配置参数比较多,但想要达到优化效果,我们并不需要全部关注。下面我们详细介绍一些主要的配置参数,保证让这只老猫跑地更快!
4.1 maxThreads
tomcat 接收客户端请求的最大线程数,也就是同时处理任务的个数,它的默认大小为 200;一般来说,在高并发的 I/O 密集型应用中,这个值设置为 1000 左右比较合理。
一般的服务器操作都包括量方面:计算 (主要消耗 cpu),等待 (io、数据库等)。
第一种极端情况,如果我们的操作是纯粹的计算,那么系统响应时间的主要限制就是 cpu 的运算能力,此时 maxThreads 应该尽量设的小,降低同一时间内争抢 cpu 的线程个数,可以提高计算效率,提高系统的整体处理能力。
第二种极端情况,如果我们的操作纯粹是 IO 操作,例如访问数据库或调用远程接口,那么响应时间的主要限制就变为等待外部资源,此时 maxThreads 应该尽量设的大,这样才能提高同时处理请求的个数,从而提高系统整体的处理能力。此情况下因为 tomcat 同时处理的请求量会比较大,所以需要关注一下 tomcat 的虚拟机内存设置和 linux 的 open file 限制。
以前一直简单的认为多线程等于高效率。其实多线程本身并不能提高 cpu 效率,线程过多反而会降低 cpu 效率。
当 cpu 核心数<线程数时,cpu 就需要在多个线程直接来回切换,以保证每个线程都会获得 cpu 时间,即通常我们说的并发执行。
所以 maxThreads 的配置绝对不是越大越好。
现实应用中,我们的操作都会包含以上两种类型 (计算、等待),所以 maxThreads 的配置并没有一个最优值,一定要根据具体情况来配置。
最好的做法是:在不断测试的基础上,不断调整、优化,才能得到最合理的配置。
4.2 maxConnections
这个参数是指在同一时间,tomcat 在任何给时间接受并处理的的最大连接数。对于 Java 的阻塞式 BIO,默认值是 maxthreads 的值;对于 Java 的 NIO 模式,maxConnections 默认值是 10000,所以这个参数我们一般保持不动即可。
当达到这个数字时,服务器将接受但不处理一个连接。这个额外的连接将被阻塞,直到正在处理的连接数量低于 maxConnections,此时服务器将开始接受并再次处理新的连接。
4.3 acceptCount
它表示当线程池中线程都在处理请求时,保存传入连接请求的最大队列长度。队列满时收到的任何请求都将被拒绝。缺省值是 100。我一般会设置成和 maxThreads 设置成一样大的。
简单说明一下上面三个参数的关系:
系统能够保持的连接数:maxConnections+acceptCount,区别是 maxConnections 中的连接可以被调度处理;acceptCount 中的连接只能等待排队。
4.4 线程池其他配置
namePrefix:每个新创建线程的名称前缀
minSpareThreads: 一直处于活跃状态的线程数
maxIdleTime:线程的空闲时间,在超过空闲时间时这些线程则会被销毁
threadPriority:线程池中线程的优先级,默认为 5
4.5 疑问
tomcat 线程池使用自己重写的 TaskQueue 任务队列,并且如果我们使用 tomcat 默认参数配置,那么 TaskQueue 的最大容量是 Integer.MAX_VALUE,如果突发请求比较多并且持续时间比较长,那么 tomcat 会不会耗尽内存而崩溃呢?
其实是不会的,因为 tomcat 在接收链接时做了限流处理。当连接达到 maxConnections 时,请求不会被 socket 接受,而是进入 TCP 的连接队列中。
于是 tomcat 处理请求的过程便是:Acceptor 接收一个请求,若现有线程数量小于 maxThreads 且没有空闲线程,则创建一个新线程处理请求任务,若超过 maxThreads,则放入 TCP 完全连接队列中 (注意,不是线程池中的队列),当队列大于 acceptCount 值时,则报“connection refused”错误。
5 总结
JDK 实现线程池功能比较完善,但是比较适合运行 CPU 密集型任务,不适合 IO 密集型的任务,对于 IO 密集型任务可以间接通过设置线程池参数方式做到。
tomcat 线程池主要是处理 web 请求 (IO 密集型),为了尽可能的处理更多的请求,所以就尽可能不让线程进行入队,提交任务时如果有空闲的线程就先用空闲的线程,线程不够用又没达到最大个数上限就去创建新线程,线程达到最大个数上限就放到队列排队去,如果队列满了执行重试机制放入队列,重试失败只能执行拒绝策略了。
公开
同步至部落
取消
完成
0/2000
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 线程池源码解读与实践》
《Java 线程池源码解读与实践》
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
精选留言
由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论