一、tomcat原理
功能组件结构
Tomcat 的核心功能有两个,分别是负责接收和反馈外部请求的连接器 Connector,和负责处理请求的容器 Container。其中连接器和容器相辅相成,一起构成了基本的 web 服务 Service。每个 Tomcat 服务器可以管理多个 Service。
连接器核心功能
一、监听网络端口,接收和响应网络请求。
二、网络字节流处理。将收到的网络字节流转换成 Tomcat Request 再转成标准的 ServletRequest 给容器,同时将容器传来的 ServletResponse 转成 Tomcat Response 再转成网络字节流。
连接器模块设计
为满足连接器的两个核心功能,我们需要一个通讯端点来监听端口;需要一个处理器来处理网络字节流;最后还需要一个适配器将处理后的结果转成容器需要的结构。
对应的源码包路径 org.apache.coyote
。对应的结构图如:
Tomcat 容器核心原理
Tomcat 容器框架——Catalina
容器结构分析
每个 Service 会包含一个容器。容器由一个引擎可以管理多个虚拟主机。每个虚拟主机可以管理多个 Web 应用。每个 Web 应用会有多个 Servlet 包装器。Engine、Host、Context 和 Wrapper,四个容器之间属于父子关系。
对应的源码包路径 org.apache.coyote
。对应的结构图如下:
容器请求处理
容器的请求处理过程就是在 Engine、Host、Context 和 Wrapper 这四个容器之间层层调用,最后在 Servlet 中执行对应的业务逻辑。各容器都会有一个通道 Pipeline,每个通道上都会有一个 Basic Valve(如StandardEngineValve), 类似一个闸门用来处理 Request 和 Response 。其流程图如下。
Tomcat 请求处理流程
上面的知识点已经零零碎碎地介绍了一个 Tomcat 是如何处理一个请求。简单理解就是连接器的处理流程 + 容器的处理流程 = Tomcat 处理流程。哈!那么问题来了,Tomcat 是如何通过请求路径找到对应的虚拟站点?是如何找到对应的 Servlet 呢?
映射器功能介绍
这里需要引入一个上面没有介绍的组件 Mapper。顾名思义,其作用是提供请求路径的路由映射。根据请求URL地址匹配是由哪个容器来处理。其中每个容器都会它自己对应的Mapper,如 MappedHost。不知道大家有没有回忆起被 Mapper class not found 支配的恐惧。在以前,每写一个完整的功能,都需要在 web.xml 配置映射规则,当文件越来越庞大的时候,各个问题随着也会出现
HTTP请求流程
打开 tomcat/conf 目录下的 server.xml 文件来分析一个http://localhost:8080/docs/api 请求。
第一步:连接器监听的端口是8080。由于请求的端口和监听的端口一致,连接器接受了该请求。
第二步:因为引擎的默认虚拟主机是 localhost,并且虚拟主机的目录是webapps。所以请求找到了 tomcat/webapps 目录。
第三步:解析的 docs 是 web 程序的应用名,也就是 context。此时请求继续从 webapps 目录下找 docs 目录。有的时候我们也会把应用名省略。
第四步:解析的 api 是具体的业务逻辑地址。此时需要从 docs/WEB-INF/web.xml 中找映射关系,最后调用具体的函数。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<!-- 连接器监听端口是 8080,默认通讯协议是 HTTP/1.1 -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<!-- 名字为 Catalina 的引擎,其默认的虚拟主机是 localhost -->
<Engine name="Catalina" defaultHost="localhost">
<!-- 名字为 localhost 的虚拟主机,其目录是 webapps-->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
</Service>
</Server>
二、tomcat线程池介绍
Tomcat是使用最广的Java Web容器,功能强大,可扩展性强。最新版本的Tomcat(5.5.17)为了提高响应速度和效率,使用了Apache Portable Runtime(APR)作为最底层,使用了APR中包含Socket、缓冲池等多种技术,性能也提高了。APR也是Apache HTTPD的最底层。可想而知,同属于ASF(Apache Software Foundation)中的成员,互补互用的情况还是很多的,虽然使用了不同的开发语言。
Tomcat 的线程池位于tomcat-util.jar文件中,包含了两种线程池方案。
方案一:使用APR的Pool技术,使用了JNI;
方案二:使用Java实现的ThreadPool。这里介绍的是第二种。如果想了解APR的Pool技术,可以查看APR的源代码。
三、tomcat线程池工作原理
tomcat线程池有如下参数:
maxThreads, 最大线程数,tomcat能创建来处理请求的最大线程数
maxSpareTHreads, 最大空闲线程数,在最大空闲时间内活跃过,但现在处于空闲,若空闲时间大于最大空闲时 间,则回收,小于则继续存活,等待被调度。
minSpareTHreads,最小空闲线程数,无论如何都会存活的最小线程数
acceptCount, 最大等待队列数 ,请求并发大于tomcat线程池的处理能力,则被放入等待队列等待被处理。
maxIdleTime, 最大空闲时间,超过这个空闲时间,且线程数大于最小空闲数的,都会被回收
tomcat原理如上图
举例说明:以上述线程池为例,一开始就创建最小空闲数的线程在池里,20个,当同一时间请求数量大于最小空闲数20,比如来了50个并发请求,那么线程池还需要创建30个线程来处理请求。这时候当请求都处理完了,持续来的请求低于50个的时候,那么当时间过了60秒,并发数还是没有达到50,那么从第50个线程开始,线程池将按照,空闲时间达到60s的,开始逐个回收,49个,48个,47个,如此回收。如果并发请求小于20个,那么线程池会回收至20个的时候,停止回收,这就是最小空闲数的作用,即使一个请求都没有,那么线程池也得保证随时都有20个。所谓空闲回收是指:一个线程在60s的时间内,一直处于等待。那么就可以判定该线程是空闲。如果这个空闲线程是在最小空闲数以上,则会被回收。当请求并发高于500最大空闲数的时候,线程池是会继续创建线程的,来满足特大突发性并发。当并发请求数降下之后,线程池中有空闲,那么,无论线程空闲时间是否达到60s,线程池都会进行回收至500。500以类的线程也会根据空闲时间是否大于60s来判断是否需要进行回收。
Tomcat线程池在工作的时候,实际情况是:
ThreadPool默认创建了5个线程,保存在一个200的线程数组中,创建时就启动了这些线程,当然在没有请求时,它们都处理“等待”状态(其实就是一个while循环,不停的等待notify)。如果有请求时,空闲线程会被唤醒执行用户的请求。
具体的请求过程是: 服务启动时,创建一个一维线程数组(maxThread=200个),并创建空闲线程(minSpareThreads=5个)随时等待用户请求。 当有用户请求时,调用 threadpool.runIt(ThreadPoolRunnable)方法,将一个需要执行的实例传给ThreadPool中。其中用户需要执行的实例必须实现ThreadPoolRunnable接口。 ThreadPool 首先查找空闲的线程,如果有则用它运行要执行ThreadPoolRunnable;如果没有空闲线程并且没有超过maxThreads,就一次性创建 minSpareThreads个空闲线程;如果已经超过了maxThreads了,就等待空闲线程了。总之,要找到空闲的线程,以便用它执行实例。找到后,将该线程从线程数组中移走。 接着唤醒已经找到的空闲线程,用它运行执行实例(ThreadPoolRunnable)。 运行完ThreadPoolRunnable后,就将该线程重新放到线程数组中,作为空闲线程供后续使用。
2.下面我们详细结合实际情况来阐述tomcat线程池在实际运用中,是如何工作的,如何处理并发的。
可以结合这个来看,最高的线程如果是繁忙的话,那么说明tomcat线程已经被打满了。
在短时间周期内,如果线程数忽然持续走高,说明有突发性请求已经打过来,且正在创建更多的线程去执行,这时候创建线程的过程中,请求处于被等待,越是最后的请求被等待的时间越长。而过一段时间,线程数降下去很多的话,说明突发性请求已经过去了,线程池里的线程空闲时间达到了最大空闲时间,比如60s,那么即将被回收。
我个人觉得,这种现象应该被归属于不健康状态。因为请求来了,如果等待的线程只有10个以下,那么等待时间不会太长。但是如果等待时间达到10个以上,等待时间就会呈几何方式上升的,且线程处理时间也会呈几何倍数上升,因为同一个线程池里的线程之间要相互竞争CPU资源,比如现在tomcat运行中有1000个线程,假如每个CPU的时间片为0.01秒,那么,轮到第1000个线程执行时,该线程实际已经等待了999个时间片*0.01=1秒了,这还只是一次时间片执行,0.01秒相当于10ms,而咱们的一个请求正常情况下的时间是:30ms,那一个请求要成功就需要等待3次cpu时间片来执行,如果tomcat线程池一直有1000个在运行,那正常情况下一个请求的执行时间应该是:0.03+999*0.01*3=3.03s,可想而知,执行一个请求只需要30ms,可是却要等待需要3s,这是相当不靠谱的且难以接受的。再假如,时间片是20ms,那么一个请求从进入等待到执行完毕的执行时间应该是多少?0.03+999*0.02*(0.03/0.02=取大于整数=2)+CPU切换时间a*999次=4.03s+999a,这已经很夸张了吧!
咱们还可以理想一点,加入线程池里只有500个活跃线程,那么上面的公式应该是:当cpu时间片为0.01s时,最后一个请求需要处理成功,需要用时:0.03+500*0.01*3= 1.53s,当cpu时间片为0.02s时,最后一个请求需要处理成功,需要用时:0.03+500*0.02*2= 2.03s,当cpu时间片为0.03s时,最后一个请求需要处理成功,需要用时:0.03+500*0.03*1= 1.53s。
但是这只是cpu执行时间,还要加上网络传输,创建线程时间,cpu切换线程所需时间,还有其他时间,总共加起来,就是一个很可怕的时间了。所以,即使tomcat有线程池,但最好不要总是超过最小空闲数,这是最优的情况,最次的情况就是tomcat线程池中线程数超过最大空闲数了,线程们总是繁忙地工作,宕机随时有可能。
而健康状态是:线程池保持最小空闲数才算是健康,如果你的tomcat线程数总是超过最小空闲数,那么你的程序随时都处于繁忙状态,这时候出错的几率已经大大增加。是时候需要被重点关注了。
四、tomcat线程池跟jdk线程池的主要区别
tomcat线程池跟jdk线程池略有区别,它的TaskQueue是一个(capacity为Integer.MAX_VALUE)无界队列,为了使线程池的线程数能达到maximumPoolSize,它的offer方法做了一些改变,当线程数达到minSpareThreads(corePoolSize)后,需要向TaskQueue(默认的capacity为Integer.MAX_VALUE,除非重新赋值)中存储,但在offer方法存储过程中,如果发现线程数没有达到maximumPoolSize,就会新开线程去处理,而不再存入TaskQueue,除非达到maximumPoolSize才会存入队列。所以tomcat线程池虽然是无界队列,但它的线程数上限却是maximumPoolSize。
tomcat任务队列
Tomcat的线程队列由org.apache.tomcat.util.threads.TaskQueue来处理,它集成自LinkedBlockingQueue(一个阻塞的链表队列),看下面源码:
public class TaskQueue extends LinkedBlockingQueue<Runnable> { @Override public boolean offer(Runnable o) { //we can't do any checks if (parent==null) return super.offer(o); //we are maxed out on threads, simply queue the object if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o); //we have idle threads, just add it to the queue if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o); //如果线程数没有达到maximumPoolSize时,就会新开线程去处理,而不存入TaskQueue if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false; //if we reached here, we need to add it to the queue return super.offer(o); } }
tomcat的线程池的名字也叫ThreadPoolExecutor,在jdk的ThreadPoolExecutor 基础上做了封装,源码如下:
成员变量:
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor { /** * The string manager for this package. */ protected static final StringManager sm = StringManager .getManager("org.apache.tomcat.util.threads.res"); //已经提交但尚未完成的任务数量。这包括已经在队列中的任务和已经交给工作线程的任务但还未开始执行的任务,这个数字总是大于getActiveCount() private final AtomicInteger submittedCount = new AtomicInteger(0); private final AtomicLong lastContextStoppedTime = new AtomicLong(0L); //最近的时间在ms时,一个线程决定杀死自己来避免潜在的内存泄漏。 用于调节线程的更新速率。 private final AtomicLong lastTimeThreadKilledItself = new AtomicLong(0L); //延迟2个线程之间的延迟。 如果为负,不更新线程 private long threadRenewalDelay = Constants.DEFAULT_THREAD_RENEWAL_DELAY;
主要的方法如下:
/** * 方法在完成给定Runnable的执行时调用。统计 * 此方法由执行任务的线程调用。 如果 * 非null,Throwable是未捕获的{@code RuntimeException} * 或{@code Error},导致执行突然终止。... * @param r 已完成的任务 * @param t 引起终止的异常,如果执行正常完成则为null **/ @Override protected void afterExecute(Runnable r, Throwable t) { submittedCount.decrementAndGet(); if (t == null) { stopCurrentThreadIfNeeded(); } } //如果当前线程在上一次上下文停止之前启动,则抛出异常,以便停止当前线程 protected void stopCurrentThreadIfNeeded() { if (currentThreadShouldBeStopped()) { long lastTime = lastTimeThreadKilledItself.longValue(); if (lastTime + threadRenewalDelay < System.currentTimeMillis()) { if (lastTimeThreadKilledItself.compareAndSet(lastTime, System.currentTimeMillis() + 1)) { // OK, it's really time to dispose of this thread final String msg = sm.getString( "threadPoolExecutor.threadStoppedToAvoidPotentialLeak", Thread.currentThread().getName()); throw new StopPooledThreadException(msg); } } } } //当前线程是否需要被终止 protected boolean currentThreadShouldBeStopped() { if (threadRenewalDelay >= 0 && Thread.currentThread() instanceof TaskThread) { TaskThread currentTaskThread = (TaskThread) Thread.currentThread(); if (currentTaskThread.getCreationTime() < this.lastContextStoppedTime.longValue()) { return true; } } return false; } /** * {@inheritDoc} */ @Override public void execute(Runnable command) { execute(command,0,TimeUnit.MILLISECONDS); } /** * 在将来的某个时候执行给定的命令。命令可以在新线程、池线程或调用线程中执行,具体由执行器实现决定。如果没有可用的线程,它将被添加到工作队列中。如果工作队列已满,系统将等待指定的时间,如果在此之后队列仍满,系统将抛出RejectedExecutionException * * @param command the runnable task * @param timeout A timeout for the completion of the task * @param unit The timeout time unit * @throws RejectedExecutionException if this task cannot be * accepted for execution - the queue is full * @throws NullPointerException if command or unit is null */ public void execute(Runnable command, long timeout, TimeUnit unit) { submittedCount.incrementAndGet(); try { super.execute(command); } catch (RejectedExecutionException rx) { if (super.getQueue() instanceof TaskQueue) { final TaskQueue queue = (TaskQueue)super.getQueue(); try { if (!queue.force(command, timeout, unit)) { submittedCount.decrementAndGet(); throw new RejectedExecutionException("Queue capacity is full."); } } catch (InterruptedException x) { submittedCount.decrementAndGet(); throw new RejectedExecutionException(x); } } else { submittedCount.decrementAndGet(); throw rx; } } } public void contextStopping() { this.lastContextStoppedTime.set(System.currentTimeMillis()); // save the current pool parameters to restore them later int savedCorePoolSize = this.getCorePoolSize(); TaskQueue taskQueue = getQueue() instanceof TaskQueue ? (TaskQueue) getQueue() : null; if (taskQueue != null) { // note by slaurent : quite oddly threadPoolExecutor.setCorePoolSize // checks that queue.remainingCapacity()==0. I did not understand // why, but to get the intended effect of waking up idle threads, I // temporarily fake this condition. taskQueue.setForcedRemainingCapacity(Integer.valueOf(0)); } // setCorePoolSize(0) wakes idle threads this.setCorePoolSize(0); // TaskQueue.take() takes care of timing out, so that we are sure that // all threads of the pool are renewed in a limited time, something like // (threadKeepAlive + longest request time) if (taskQueue != null) { // ok, restore the state of the queue and pool taskQueue.setForcedRemainingCapacity(null); } this.setCorePoolSize(savedCorePoolSize); } //拒绝策略,抛RejectedExecutionException private static class RejectHandler implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, java.util.concurrent.ThreadPoolExecutor executor) { throw new RejectedExecutionException(); } }
再看ThreadFactory类:
public class TaskThreadFactory implements ThreadFactory { private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; private final boolean daemon; private final int threadPriority; public TaskThreadFactory(String namePrefix, boolean daemon, int priority) { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); this.namePrefix = namePrefix; this.daemon = daemon; this.threadPriority = priority; } @Override public Thread newThread(Runnable r) { TaskThread t = new TaskThread(group, r, namePrefix + threadNumber.getAndIncrement()); t.setDaemon(daemon); t.setPriority(threadPriority); // Set the context class loader of newly created threads to be the class // loader that loaded this factory. This avoids retaining references to // web application class loaders and similar. if (Constants.IS_SECURITY_ENABLED) { PrivilegedAction<Void> pa = new PrivilegedSetTccl( t, getClass().getClassLoader()); AccessController.doPrivileged(pa); } else { t.setContextClassLoader(getClass().getClassLoader()); } return t; } }
Tomcat的线程类:
Tomcat自己定义了TaskThread用于线程的执行,里面增加了creationTime字段用于定义线程创建的开始时间,以便后面线程池获取这个时间来进行优化。
//一个实现创建时间纪录的线程类 public class TaskThread extends Thread { private static final Log log = LogFactory.getLog(TaskThread.class); private final long creationTime; public TaskThread(ThreadGroup group, Runnable target, String name) { super(group, new WrappingRunnable(target), name); this.creationTime = System.currentTimeMillis(); } public TaskThread(ThreadGroup group, Runnable target, String name, long stackSize) { super(group, new WrappingRunnable(target), name, stackSize); this.creationTime = System.currentTimeMillis(); } /** * Wraps a {@link Runnable} to swallow any {@link StopPooledThreadException} * instead of letting it go and potentially trigger a break in a debugger. */ private static class WrappingRunnable implements Runnable { private Runnable wrappedRunnable; WrappingRunnable(Runnable wrappedRunnable) { this.wrappedRunnable = wrappedRunnable; } @Override public void run() { try { wrappedRunnable.run(); } catch(StopPooledThreadException exc) { //expected : we just swallow the exception to avoid disturbing //debuggers like eclipse's log.debug("Thread exiting on purpose", exc); } } } }
按照Tomcat的注解可知,它就是一个普通的线程类,然后增加一个纪录线程创建的时间纪录而已,后面还使用动态内部类封装了一个Runnable,用于调试出发中断
参考:
https://zhuanlan.zhihu.com/p/248426114
https://blog.csdn.net/li396864285/article/details/49331369
https://blog.csdn.net/qq_33685675/article/details/79032762