原生线程池这么强大,Tomcat 为何还需扩展线程池?
前言
Tomcat/Jetty 是目前比较流行的 Web 容器,两者接受请求之后都会转交给线程池处理,这样可以有效提高处理的能力与并发度。JDK 提高完整线程池实现,但是 Tomcat/Jetty 都没有直接使用。Jetty 采用自研方案,内部实现 QueuedThreadPool 线程池组件,而 Tomcat 采用扩展方案,踩在 JDK 线程池的肩膀上,扩展 JDK 原生线程池。
JDK 原生线程池可以说功能比较完善,使用也比较简单,那为何 Tomcat/Jetty 却不选择这个方案,反而自己去动手实现那?
JDK 线程池
通常我们可以将执行的任务分为两类:
- cpu 密集型任务
- io 密集型任务
cpu 密集型任务,需要线程长时间进行的复杂的运算,这种类型的任务需要少创建线程,过多的线程将会频繁引起上文切换,降低任务处理处理速度。
而 io 密集型任务,由于线程并不是一直在运行,可能大部分时间在等待 IO 读取/写入数据,增加线程数量可以提高并发度,尽可能多处理任务。
JDK 原生线程池工作流程如下:

详情可以查看 一文教你安全的关闭线程池,上图假设使用
LinkedBlockingQueue。
灵魂拷问:上述流程是否记错过?在很长一段时间内,我都认为线程数量到达最大线程数,才放入队列中。 ̄□ ̄||
上图中可以发现只要线程池线程数量大于核心线程数,就会先将任务加入到任务队列中,只有任务队列加入失败,才会再新建线程。也就是说原生线程池队列未满之前,最多只有核心线程数量线程。
这种策略显然比较适合处理 cpu 密集型任务,但是对于 io 密集型任务,如数据库查询,rpc 请求调用等,就不是很友好了。
由于 Tomcat/Jetty 需要处理大量客户端请求任务,如果采用原生线程池,一旦接受请求数量大于线程池核心线程数,这些请求就会被放入到队列中,等待核心线程处理。这样做显然降低这些请求总体处理速度,所以两者都没采用 JDK 原生线程池。
解决上面的办法可以像 Jetty 自己实现线程池组件,这样就可以更加适配内部逻辑,不过开发难度比较大,另一种就像 Tomcat 一样,扩展原生 JDK 线程池,实现比较简单。
下面主要以 Tomcat 扩展线程池,讲讲其实现原理。
扩展线程池
首先我们从 JDK 线程池源码出发,查看如何这个基础上扩展。

可以看到线程池流程主要分为三步,第二步根据 queue#offer 方法返回结果,判断是否需要新建线程。
JDK 原生队列类型 LinkedBlockingQueue, SynchronousQueue,两者实现逻辑不尽相同。
LinkedBlockingQueue
offer 方法内部将会根据队列是否已满作为判断条件。若队列已满,返回 false,若队列未满,则将任务加入队列中,且返回 true。
SynchronousQueue
这个队列比较特殊,内部不会储存任何数据。若有线程将任务放入其中将会被阻塞,直到其他线程将任务取出。反之,若无其他线程将任务放入其中,该队列取任务的方法也将会被阻塞,直到其他线程将任务放入。
对于 offer 方法来说,若有其他线程正在被取方法阻塞,该方法将会返回 true。反之,offer 方法将会返回 false。
所以若想实现适合 io 密集型任务线程池,即优先新建线程处理任务,关键在于 queue#offer 方法。可以重写该方法内部逻辑,只要当前线程池数量小于最大线程数,该方法返回 false,线程池新建线程处理。
当然上述实现逻辑比较糙,下面我们就从 Tomcat 源码查看其实现逻辑。
Tomcat 扩展线程池
Tomcat 扩展线程池直接继承 JDK 线程池 java.util.concurrent.ThreadPoolExecutor,重写部分方法的逻辑。另外还实现了 TaskQueue,直接继承 LinkedBlockingQueue,重写 offer 方法。
首先查看 Tomcat 线程池的使用方法。

可以看到 Tomcat 线程池使用方法与普通的线程池差不太多。
接着我们查看一下 Tomcat 线程池核心方法 execute 的逻辑。

execute 方法逻辑比较简单,任务核心还是交给 Java 原生线程池处理。这里主要增加一个重试策略,如果原生线程池执行拒绝策略的情况,抛出 RejectedExecutionException 异常。这里将会捕获,然后重新再次尝试将任务加入到 TaskQueue ,尽最大可能执行任务。
这里需要注意 submittedCount 变量。这是 Tomcat 线程池内部一个重要的参数,它是一个 AtomicInteger 变量,将会实时统计已经提交到线程池中,但还没有执行结束的任务。也就是说 submittedCount 等于线程池队列中的任务数加上线程池工作线程正在执行的任务。 TaskQueue#offer 将会使用该参数实现相应的逻辑。
接着我们主要查看 TaskQueue#offer 方法逻辑。

核心逻辑在于第三步,这里如果 submittedCount 小于当前线程池线程数量,将会返回 false。上面我们讲到 offer 方法返回 false,线程池将会直接创建新线程。
Dubbo 2.6.X 版本增加 EagerThreadPool,其实现原理与 Tomcat 线程池差不多,感兴趣的小伙伴可以自行翻阅。
折衷方法
上述扩展方法虽然看起不是很难,但是自己实现代价可能就比较大。若不想扩展线程池运行 io 密集型任务,可以采用下面这种折衷方法。
new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(100));
不过使用这种方式将会使 keepAliveTime 失效,线程一旦被创建,将会一直存在,比较浪费系统资源。
总结
JDK 实现线程池功能比较完善,但是比较适合运行 CPU 密集型任务,不适合 IO 密集型的任务。对于 IO 密集型任务可以间接通过设置线程池参数方式做到。
欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn

原生线程池这么强大,Tomcat 为何还需扩展线程池?的更多相关文章
- Tomcat线程池,更符合大家想象的可扩展线程池
因由 说起线程池,大家可能受连接池的印象影响,天然的认为,它应该是一开始有core条线程,忙不过来了就扩展到max条线程,闲的时候又回落到core条线程,如果还有更高的高峰,就放进一个缓冲队列里缓冲一 ...
- 详解tomcat的连接数与线程池
前言 在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector). 在前面的文章 详解Tomcat配置文件server.xm ...
- 详解Tomcat的连接数和线程池
转: https://www.cnblogs.com/kismetv/p/7806063.html#t11 前言 在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须 ...
- 【Tomcat】详解tomcat的连接数与线程池
前言 在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector). Connector的主要功能,是接收连接请求,创建Req ...
- 详解 Tomcat 的连接数与线程池
前言 在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector). 在前面的文章 详解Tomcat配置文件server. ...
- tomcat的连接数与线程池
在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector). 在前面的文章中写到过:Connector的主要功能,是接收连接 ...
- 【转】详解tomcat的连接数与线程池
对tomcat线程池.Connector的BIO.NIO解析的很透彻的一篇文章. 原文链接:https://www.cnblogs.com/kismetv/p/7806063.html 前言 在使用t ...
- 用 ThreadPoolExecutor/ThreadPoolTaskExecutor 线程池技术提高系统吞吐量(附带线程池参数详解和使用注意事项)
1.概述 在Java中,我们一般通过集成Thread类和实现Runnnable接口,调用线程的start()方法实现线程的启动.但如果并发的数量很多,而且每个线程都是执行很短的时间便结束了,那样频繁的 ...
- 持续集成环境--Tomcat热部署导致线程泄漏
一.问题由来 我们组用jenkins部署了持续集成环境,(jenkins部署war包到远程服务器的tomcat). 每次提交了代码,jenkins上一键构建,就可以自动拉取最新代码,打war包,热部署 ...
随机推荐
- SYZOJ中文安装指南
Made By:Spaceskynet Thanks to other developers. 测试系统 Ubuntu-17.04 PS(全局变量): [syzoj2 path] = 您git的syz ...
- javadoc的使用
在进行项目开发过程中,项目接口文档是很重要的一块内容,在java项目中我们可以用swagger,asciidoc,javadoc等方式来生产文档,而其中最基本的文档生成方式就是javadoc,它一般用 ...
- 如何往Spark社区做贡献,贡献代码
随着社区正在努力准备Apache Spark的下一版本3.0,您可能会问自己“我如何参与其中?”.现在的Spark代码已经很庞大,因此很难知道如何开始自己做出贡献.Spark PMC & Co ...
- 浅谈Retinex
Retinex是上个世纪七十年代由Land提出的色彩理论.我认为其核心思想基于俩点 (1)在颜色感知时,人眼对局部相对光强敏感程度要优于绝对光强. (2)反射分量R(x,y)储存有无光源物体的真实模样 ...
- 为什么 Flutter 是跨平台开发的终极之选
跨平台开发是当下最受欢迎.应用最广泛的框架之一.能实现跨平台开发的框架也五花八门,让人眼花缭乱.最流行的跨平台框架有 Xamarin.PhoneGap.Ionic.Titanium.Monaca.Se ...
- C# 倒计时,显示天,时,分,秒。时间可以是从数据库捞出来
从数据库把时间读出来,接着 你用个timer控件启用控件,设置1000毫秒timer时间里 用当前时间-你取出的时间 就可以了 DateTime furtime = Convert.ToDateTim ...
- NOIP模拟 25
分层考试第一场. 垫底. T1 lighthouse 观察到m很小,想到容斥. 正常人都想枚举子集,只有我打了搜索. 为了压行,我压缩了几句分类讨论. 压错了,原地爆炸 考场思路: 不容斥这也不可做啊 ...
- python学习之【第二篇】:Python中的数字及其所具有的方法
1.前言 Python 数字(number)数据类型用于存储数值.数据类型是不允许改变的,这就意味着如果改变数字数据类型的值,将重新分配内存空间. 2.创建数字对象 以下实例在变量赋值时 Number ...
- GC四大算法
引言: 前面的文章提到,Heap包括了PSYoungGen.ParOldGen.Metaspace.JVM 在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是新生代.由于新生代 ...
- 易初大数据 2019年11月14日 spss笔记 王庆超
“均匀分布”的随机数 需要打开本章的数据文件“sim.sav.”. 1.设置随机数种子 1选择[转换]--[随机数字生成器],勾选‘设置起点’,并在‘固定值’ 的下‘值’中输入一个用户给定的数值.该数 ...