Tomcat线程池简单介绍
前言
tomcat的线程池扩展了jdk的executor,而且队列用的是自己的task queue,因此其策略与jdk的有所不同。
本篇将讨论一下tomcat线程池和jdk线程池的不同之处,以及tomcat为什么要重写jdk线程池的方法。
一、Tomcat的请求处理过程
一个客户端请求到达Tomcat之后的处理流程如上图所示:
- 当Tomcat启动后,Connector的接收器Acceptor会监听是否有客户端连接。
- 一旦监听到客户端连接,则将连接交给线程池Executor,开始执行请求响应任务。
- Http11Processor负责从客户端连接中读取Http报文并进行解析,解析后的报文封装成Request对象。
- Maper根据Http协议请求的URL值和Host属性匹配由哪个Host、哪个Context和哪个Wrapper容器来处理请求。
- CoyoteAdaptor负责将Connector组件和Engine容器连接起来,将Request对象和Response对象传递到Engine容器中。
- Engine容器的请求处理管道开始工作,管道里包括若干Valve,每个Valve都负责一些处理逻辑。
- Engine容器的请求处理管道工作完成后,再依次交给Host容器的处理管道、Context容器的处理管道和Wrapper容器的处理管道。最后将结果输出到客户端。
二、tomcat在哪里用到了线程池
说明
- LimitLatch用来限流,可以控制最大连接个数
- acceptor负责接收新的socket连接
- poller负责监听socket channel是否有可读的IO事件
- 一旦有可读的IO事件被监听到,就封装一个任务对象socketProcessor,提交给executor线程池处理
- executor线程池中的工作线程最终负责处理请求
三、Tomcat线程池和JUC线程池流程
JDK线程池策略:
- 当线程池中线程数量小于corePoolSize,每来一个任务,就会创建一个线程执行这个任务。
- 当前线程池线程数量大于等于corePoolSize,则每来一个任务。会尝试将其添加到任务缓存队列中,若是添加成功,则该任务会等待线程将其取出去执行;若添加失败(一般来说任务缓存队列已满),则会尝试创建新的线程执行。
- 当前线程池线程数量等于maximumPoolSize,则会采取任务拒绝策略进行处理。
tomcat线程池策略:
- 当前线程数小于corePoolSize,则去创建工作线程;
- 当前线程数大于corePoolSize,但小于maximumPoolSize,则去创建工作线程;
- 当前线程数大于maximumPoolSize,则将任务放入到阻塞队列中,当阻塞队列满了之后,则调用拒绝策略丢弃任务;
Tomcat线程池对JUC线程池的增强
Tomcat是重写了JDK线程池(即改动的是少量的源码)实现的功能的增强。
主要流程还是JDK线程池流程,即先开启corePoolSize线程,然后在queue,最后在开启maximumPoolSize线程。
Tomcat重点改造的是queue的offer()。即在向queue放入任务时,若发现未达到最大线程数,那么offer()返回false,即放入队列失败。此时,便继续开启maximumPoolSize线程。
那么Tomcat为什么要重新改造JDK线程池呢?
四、Tomcat线程池和JUC线程池的区别
使用线程池的任务有两种:
- IO密集型任务(如调用接口、查询数据库);
- CPU密集型任务;
场景
JDK线程池:当线程数达到corePoolSize后,任务首先被放到queue。发挥CPU多核的并行优势,减少多个线程导致的上下文切换。
适合的场景是:CPU密集型任务
Tomcat线程池:当大量请求达到时,接收的请求数量大于核心线程池的corePoolSize时,会继续创建worker线程去处理请求。而后续请求量变少时,只会销毁maximumPoolSize线程数。
适合的场景是:IO密集型任务。
JDK的线程池ThreadPoolExecutor
主要目的解决的便是CPU密集型任务的并发处理。但是Tomcat若使用原生的JDK线程池,一旦接收的请求数量大于线程池的核心线程数,这些请求就会被放到队列中,等待核心线程处理。这样会降低请求的总体处理速度,所以Tomcat并没有使用JDK原生线程池的策略。
Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同
- 如果总线程数达到 maximumPoolSize
- 这时不会立刻抛 RejectedExecutionException 异常
- 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常