Tomcat线程模型及设计精髓
一、Tomcat架构模型
1.1 Tomcat架构模型
我们知道Tomcat既要接收客户端请求,同时需要处理业务逻辑,做相关的转发。
那么Server端无疑需要根据其能力,划分为2个部分。
- 连接器
连接器主要处理请求,并将请求根据路径做转发。默认情况下最大连接数为8192。连接器就需要实现上述功能。
Tomcat同时支持3中连接配置:- BIO(Blocking IO)阻塞式IO(Tomcat8.5后弃用)
- NIO(NioEndpoint )(使用Epoll模型,当连接、读写事件完成后,回调阻塞的线程,让内核任务继续完成)
- AIO (Nio2Endpoint)(Nio2异步非阻塞式IO,win系统可以实现,但JVM并未在Linux系统实现异步能力,Nio及Nio2仍然使用Epoll实现)
- 连接器的主从模型
maxConnections:与tomcat建立的最大socket连接数,默认8192
服务器需要大的连接数,根据Doug Lea的可扩展IO逻辑,现在的网络IO项目均采用主从线程模型,详细信息可查Nio线程模型详解
- Engine容器
Engine容器,实现多层包装的容器管道,管道中可以自定义实现能力,就像Nio中的管道In/OutBound处理器,分部完成各层业务逻辑。图见1.2链路模型。
1.2 链路模型
二、Tomcat线程模型
2.1 同步/异步、阻塞/非阻塞模型
- 何为同步、何为异步?
在Java开发中,我们尝尝为了提高开发效率,使用Future<?> task或CompletableFuture链式变成的方式submit执行任务,此时是异步提交任务,最终执行的结果再通过get(TimeUnits)的方式获取到,此时便是异步。
一般不使用线程池,同步等待任务结果的便是同步。 - 何为阻塞式IO、何为非阻塞式IO?
很多人爱讲现实中烧茶喝水的案例来解释,当烧水时我站边旁边等待则是阻塞式的,我不在旁边等待而是去看电视了则是非阻塞式的,很贴切很形象,但似乎并未完全解释其定义。结合IO模型中read()方法,我们来看:- 阻塞式IO: 当执行read()方法后(此时CPU尚在用户态),CPU调用系统函数,客户端发送的数据尚未从网卡拷贝至用户态内存空间,此时则需要静静地等待数据。
- 非阻塞式IO:当CPU在用户态执行read()方法后,不再等待客户端发送数据和数据从内核态到用户态的传输。当数据到达用户空间时会主动通知线程干活。
2.2 从硬件看Tomcat连接线程模型
此处用最常用的Nio同步非阻塞模型来看
- 蓝色线表示Server端执行accept()及read()方法的过程
- 紫色线表示当用户传输数据到网卡后,操作系统执行的过程。
数据来到网卡后,只有内核空间可以执行对硬件的操作(将数据读取到),并回调注册了回调方法的阻塞等待任务,此时task_struct任务再次被激活,并来到运行任务队列,当CPU执行到该任务时,将线程上下文信息恢复到寄存器。CPU从内核态切换到用户态,将内核空间的数据拷贝至用户态堆数据中。(当然由于mmap映射区的存在,一些场景下可以实现不拷贝数据,而知识获取内核空间的地址,从然实现获取、修改内容的能力,进而实现零拷贝)
三、Tomcat设计精髓
<Service name="Catalina">
<!--
namePrefix: 线程前缀
maxThreads: 最大线程数,默认设置 200,一般建议在 500 ~ 800,根据硬件设施和业务来判断
minSpareThreads: 核心线程数,默认设置 25
prestartminSpareThreads: 在 Tomcat 初始化的时候就初始化核心线程
maxQueueSize: 最大的等待队列数,超过则拒绝请求 ,默认 Integer.MAX_VALUE
maxIdleTime: 线程空闲时间,超过该时间,线程会被销毁,单位毫秒
className: 线程实现类,默认org.apache.catalina.core.StandardThreadExecutor
-->
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-%d"
prestartminSpareThreads="true"
maxThreads="500" minSpareThreads="8" maxIdleTime="10000"/>
<!--
maxThreads: 内置线程池最大线程数,默认设置 200
minSpareThreads: 内置线程池核心线程数,默认设置 10
maxConnections:与tomcat建立的最大socket连接数,默认8192
acceptCount: 操作系统底层请求队列的最大长度 ,默认值为100
-->
<Connector port="8080" protocol="HTTP/1.1" executor="tomcatThreadPool"
connectionTimeout="20000" maxConnections="8192" maxThreads="500"
redirectPort="8443" URIEncoding="UTF-8"/>
<Engine xxx/>
......
</Service>
3.1 LimitLatch限流计数器
/**
- Shared latch that allows the latch to be acquired a limited number of times
- after which all subsequent requests to acquire the latch will be placed in a
- FIFO queue until one of the shares is returned.
*/
上文说过Tomcat因为最大线程数的限制,必须针对request请求进行限流,那么限流的方式如何?
LimitLatch需要实现何种功能?
1、计数器
2、计数自增,当达到预设限制时,需要拒绝
我们对比Tomcat限流器LimitLatch和Doug Lea的CountDownLatch类结构比较。我们知道CountDownLatch的作用是闭锁AQS实现及各种锁详细
CountDownLatch,在计数count量减1,直至所有均完成时结束。而次数需要做限流,则需要count自增,直至count来到Limit。且看Tomcat的核心修改如下。
- 尝试获取锁:AtomicLong自增,当请求数超过限制时返回-1,并修正原有count
@Override
protected int tryAcquireShared(int ignored) {
long newCount = count.incrementAndGet();
if (!released && newCount > limit) {
// Limit exceeded
count.decrementAndGet();
return -1;
} else {
return 1;
}
}
3.2 Tomcat线程池无界队列实现
3.2.1 JDK线程池实现
JDK常规的线程池需要先把Task交给核心线程,当核心线程数未满时则创建核心线程。核心线程池满了则将Task交给Queue,如何阻塞队列也满了,此时才创建非核心线程。下面是此前整理的线程池详解中总结的线程池流程图。
看JDK线程池执行任务源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//Step1 如果运行的核心线程数<定义核心线程数,则创建核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//Step2 判断线程池是否仍在运行,尝试加入到队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//Step3 Queue.offer添加任务队列失败时才会添加非核心线程执行command任务
else if (!addWorker(command, false))
reject(command);
}
3.2.2 Tomcat无界队列线程池实现
- 创建Tomcat线程池:
public void createExecutor() {
internalExecutor = true;
//注意看队列队列使用的是Tomcat自定义实现的LinkedBlockingQueue
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent( (ThreadPoolExecutor) executor);
}
- execute方法虽然与JDK的实现略微有调整,但实际执行无异。
- 核心线程和非核心线程的定义
maxThreads: 最大线程数,默认设置 200,一般建议在 500 ~ 800,根据硬件设施和业务来判断 minSpareThreads: 核心线程数,默认设置 25
- 再看 workQueue.offer(command) [实际执行TaskQueue的offer()]
@Override
public boolean offer(Runnable o) {
//we can't do any checks
if (parent==null) return super.offer(o);
//当前线程数已达到最大线程数,新的任务添加到任务队列
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
//当前有空闲线程,只需将其添加到任务队列
if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
//当前非核心线程数未满,返回false,强制创建线程
if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
//if we reached here, we need to add it to the queue
return super.offer(o);
}
是的,需要再次摩拜Doug Lea,大神的代码处处有惊喜!