文章目录
- 前言
- 正文
-
- 一、从启动脚本开始分析
- [二、ProtocolHandler 的启动原理](#二、ProtocolHandler 的启动原理)
- [三、AbstractEndPoint 的启动原理](#三、AbstractEndPoint 的启动原理)
- 四、创建默认线程池
- 五、参数配置原理
-
- [5.1 常规的参数配置](#5.1 常规的参数配置)
- [5.2 自定义线程池](#5.2 自定义线程池)
- [5.3 测试自定义线程](#5.3 测试自定义线程)
前言
在Java Web的开发过程中,Tomcat常用的web容器。SpringBoot之前,我们用的是单独的 Tomcat,SpringBoot时代,嵌入了Tomcat。
在Jdk中,JUC内有线程框架,以及可以自定义参数配置的 TreadPoolExecutor。Tomcat内也实现了自己的线程池。
所谓线程池,是被用来处理传入的 HTTP 请求的。
当客户端发送请求时,Tomcat 会从线程池中获取一个可用的线程来处理该请求。处理完请求后,线程将返回线程池,并在下一个请求到来时再次被重用。
究其原因,是JUC内的线程池不符合Tomcat的使用场景。
- Jdk中的线程池,是cpu密集型(也就是偏计算,处理完了可以去队列再取任务)
- Tomcat的应用场景,却大多是IO密集型的。(也就是要求IO尽量不要阻塞,任务先处理,实在处理不了了,再进阻塞队列)
下图是JUC中线程池处理任务的流程:
与JUC中明显不同的一点是,Tomcat为了处理IO,减少阻塞的情况,
本系列文章就是专门探讨Tomcat中线程池的原理,分为上下两篇,本文是上篇,主要介绍Tomcat中线程池的初始化原理。
本系列文章基于SpringBoot2.7.6,其内嵌的tomcat版本是9.0.69。
同系列文章:Tomcat线程池原理(下篇:工作原理)
正文
本系列文章核心内容是Tomcat的线程池原理,因此在画图,文字描述时会忽略部分不涉及的内容。
一、从启动脚本开始分析
使用过Tomcat的同学都知道,我们单独的启动tomcat时,是从脚本入手的。
启动tomcat , 需要调用 bin/startup.bat (在linux 目录下 , 需要调用 bin/startup.sh)
在startup.bat 脚本中, 调用了catalina.bat。
在catalina.bat 脚本文件中,调用了BootStrap 中的main方法。
后续的操作如下图:
简而言之,就是逐级的 init()
和 start()
。
而本文的关注点,就是 ProtocolHandler
的 start()
,也就是图中的最后一步。
二、ProtocolHandler 的启动原理
关键在于 EndPoint
的 start()
。
而在Tomcat 中,会执行到 AbstractEndPoint
的 start()
。具体代码如下:
java
public final void start() throws Exception {
if (bindState == BindState.UNBOUND) {
bindWithCleanup();
bindState = BindState.BOUND_ON_START;
}
startInternal();
}
public abstract void startInternal() throws Exception;
也就是说真正的启动方法是AbstractEndPoint
子类实现的startInternal()
。
三、AbstractEndPoint 的启动原理
在Tomcat中,有3个AbstractEndPoint
的子类。
在8.5/9.0版本中,使用的是其中的 NioEndPoint
类。
本文就使用默认的 NioEndPoint
进行分析。
接第二小节, NioEndPoint
在执行startInternal()
时,会判断是否存在线程池,如果没有,会创建默认的线程池。对应代码如下:
java
@Override
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
if (socketProperties.getProcessorCache() != 0) {
processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getProcessorCache());
}
if (socketProperties.getEventCache() != 0) {
eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getEventCache());
}
if (socketProperties.getBufferPool() != 0) {
nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getBufferPool());
}
// 如果没自定义线程池,则创建默认工作线程池
if (getExecutor() == null) {
createExecutor();
}
initializeConnectionLatch();
// Start poller thread
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-Poller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
startAcceptorThread();
}
}
四、创建默认线程池
根据第三小节的分析,在没自定义线程池,或者配置线程池时,会自动创建一个线程池。代码如下:
java
public void createExecutor() {
internalExecutor = true;
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);
}
注意,ThreadPoolExecutor 不是JUC中的线程池了,其是Tomcat自己实现的线程池。
五、参数配置原理
日常工作中,总会遇到需要自己制定Tomcat线程池参数的情况。这一小节就来说明一下。
在Tomcat中,TomcatWebServerFactoryCustomizer
负责配置自定义参数。
在自动配置类 EmbeddedWebServerFactoryCustomizerAutoConfiguration
中配置了如下内容:
java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
public static class TomcatWebServerFactoryCustomizerConfiguration {
@Bean
public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
}
}
5.1 常规的参数配置
普通的参数配置可以参考ServerProperties
中的内容。
properties
# Tomcat连接数相关参数
# 最大连接数,默认8192,一般要大于(tomcat.threads.max + tomcat.accept-count)
server.tomcat.max-connections=300
# 当所有工作线程都被占用时,新的连接将会放入等待队列中的最大容量,默认100
server.tomcat.accept-count=50
# Tomcat线程池相关参数
# 最大线程池大小,默认200
server.tomcat.threads.max=200
# 最小工作空闲线程数(核心线程数),默认10
server.tomcat.threads.min-spare=12
5.2 自定义线程池
如果普通的参数配置,不能满足你的需求,则需要自定义线程池。
定义自己的类,继承 TomcatWebServerFactoryCustomizer
,然后重写customize
即可。
核心思路是,在AbstractProtocol
中设置线程池。
以下是我的示例:
java
package org.feng.demos.web;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.ProtocolHandler;
import org.apache.tomcat.util.threads.TaskQueue;
import org.apache.tomcat.util.threads.TaskThreadFactory;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer;
import org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 自定义tomcat线程池
*
* @author feng
*/
@Component
public class MyTomcatWebServerFactoryCustomizer extends TomcatWebServerFactoryCustomizer {
public MyTomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
super(environment, serverProperties);
}
@Override
public void customize(ConfigurableTomcatWebServerFactory factory) {
super.customize(factory);
// 自定义tomcat线程池
System.out.println("自定义tomcat线程池--start");
// 自定义tomcat线程池
factory.addConnectorCustomizers((connector) -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof AbstractProtocol) {
AbstractProtocol protocol = (AbstractProtocol) handler;
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory("feng" + "-exec-", true, 5);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60L, TimeUnit.SECONDS, taskqueue, tf);
protocol.setExecutor(threadPoolExecutor);
taskqueue.setParent(threadPoolExecutor);
}
});
System.out.println("自定义tomcat线程池--end");
}
}
5.3 测试自定义线程
定义如下方法:
java
// http://127.0.0.1:8080/hello?name=lisi
@RequestMapping("/hello")
@ResponseBody
public String hello(@RequestParam(name = "name", defaultValue = "unknown user") String name) {
System.out.println("当前线程名:" + Thread.currentThread().getName());
return "Hello " + name;
}
调用时,控制台打印: