tomcat作为最受欢迎的web服务器之一,他肯定是支持并发连接以及处理请求的。
1、java线程
在开始分析tomcat线程模型前,简单回顾一下java的多线程编程。在java中。创建一个多线程程序可以直接实现Runnable接口或者继承Thread类并覆盖run方法。如下所示:
public class MyThread extends Thread {
@Override
public void run() {
//TODO
}
}
然后调用对象的start()方法启动线程,但由于java是单继承体系,所以一般通过实现Runnable接口来编写多线程。你如果想线程执行完毕时有一个返回结果,那么可以实现Callable接口,并覆盖call方法。
但这样做,线程的启动以及停止等都需要程序员来完成,所以可以使用线程池来统一启动、停止线程和管理。 这也就是java的Executor类,它是一个接口,存在于java.util.concurrent包中,可以使用以下代码创建一个线程池:
ExecutorService exec = Executors.newCachedThreadPool();
点开newCachedThreadPool()方法:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到最终使用了ThreadPoolExecutor类来创建,其中ExecutorService是Executor的子接口,而ThreadPoolExecutor是Executor的一个实现类。ThreadPoolExecutor的第一个参数表示要保持多少个线程在线程池中,参数2表示线程池中最多能有多少个线程,如果超出这个值,新加入的线程将会被放到最后一个参数Queue中去,第3以及第4个参数表示一个超时时间。
简单回顾到这儿,但请记住Executor、ThreadPoolExecutor类以及ThreadPoolExecutor的参数1以及参数2。
2、tomcat中的多线程
2.1、StandardService中的Executor
tomcat运行时会创建一个Server实例,而Server实例会启动多个Service,Service中的Connector用于接收、解析请求并交给Service中的Container处理请求。在StandardService中定义连接器以及容器代码如下:
protected Connector connectors[] = new Connector[0];
private final Object connectorsLock = new Object();
/**
*
*/
protected ArrayList<Executor> executors = new ArrayList<Executor>();
可以看到在容器的定义下方有一个Executor的定义,但这个Executor并不是上面提到的Executor,它是java.util.concurrent.Executor的一个子接口(为了易于表达,tomcat中的Executor简称为Executor,java自带的称为java的Executor),同样的,tomcat还有一个ThreadPoolExecutor实现了相应的java.util.concurrent.ThreadPoolExecutor.同时StandardService中也有相应的getExecutor,addExecutor、findExecutor方法。
全文搜索addExecutor方法,可以发现在org.apache.catalina.mbean.ServiceMBean的addExecutor方法中,Service的addExecutor被调用了。打开Executor类结构,它只有一个实现类即StandardThreadExecutor,可以注意到这个类中持有一个ThreadPoolExecutor的实例。
/**
* The executor we use for this component
*/
protected ThreadPoolExecutor executor = null;
由于tomcat中使用了Lifecycle来管理生命周期,所以,该接口的几个生命周期方法被调用时都会先调用相应的xxxInternal方法,例如,start()方法调用时会先调用startInternal方法,init方法调用时会先调用initInternal方法,点开该类的startInternal方法中可以看到ThreadPoolExecutor被初始化了,代码如下:
@Override
protected void startInternal() throws LifecycleException {
taskqueue = new TaskQueue(maxQueueSize);
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
executor.setThreadRenewalDelay(threadRenewalDelay);
if (prestartminSpareThreads) {
executor.prestartAllCoreThreads();
}
taskqueue.setParent(executor);
setState(LifecycleState.STARTING);
}
可以看到,这一个线程池中线程的数量由getMinSpareThreads()与getMaxThreads()决定,而该类中关于这两个属性如下:
/**
* max number of threads
*/
protected int maxThreads = 200;
/**
* min number of threads
*/
protected int minSpareThreads = 25;
所以可以得出结论,如果在server.xml中配置了Executor标签,以及配置了Executor的maxThreads以及minSpareThreads属性,那么StandardSerivce中Executor的线程数量由此决定,如果没有配置,则默认数量区间为25 ~ 200,当然如果server.xml标签中没有配置Executor标签,那么StandardService中executors的size为0.
可能你已经发现了,StandardService只是负责Executor的启动,添加等操作,翻遍整个类,都没有找到Executor被使用的方法。这儿可以先记住StandardService中有一组Executor,同时也记住Executor中线程数量在25 ~ 200之间。
2.2、Connector
Connector组件作用为接收请求,解析请求以及将请求交由容器处理,但tomcat7版本中的Connector并没有亲自处理,而是委托给了ProtocolHandler处理,点开ProtocolHandler的类结构:
从图上可以看到tomcat提供了3中ProtocolHandler,这3中处理请求区别如下:
Http11Protocol:即使用传统的java IO来处理请求,会为每一个请求开启一条线程来处理。这样开销比较大,支持的并发数也比较少,tomcat7以下默认是使用这个类来处理Http请求。
Http11NioProtocol:使用java 1.4以后的NIO来处理请求,NIO是基于缓存的、非阻塞的方式,同时支持更高的并发,tomcat8中默认使用NIO处理。关于NIO相比BIO(传统IO)的更多优势这儿就不在一一介绍了,有兴趣的可以百度。
Http11NioProtocol:使用java 1.4以后的NIO来处理请求,NIO是基于缓存的、非阻塞的方式,同时支持更高的并发,tomcat8中默认使用NIO处理。关于NIO相比BIO(传统IO)的更多优势这儿就不在一一介绍了,有兴趣的可以百度。
Http11AprProtocol:简单理解,就是从操作系统级别解决异步IO问题,大幅度的提高服务器的处理和响应性能, 也是Tomcat运行高并发应用的首选模式。
所以Connector采用哪一种接收请求处理方式会在极大程度上影响tomcat的并发性。那么Connector到底使用哪一种方式呢?这取决于配置,下面是Connector的的构造函数:
public Connector(String protocol) {
setProtocol(protocol);
// Instantiate protocol handler
try {
Class<?> clazz = Class.forName(protocolHandlerClassName);
this.protocolHandler = (ProtocolHandler) clazz.newInstance();
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed"), e);
}
}
这儿可以看出在Connector中使用反射创建了一个ProtocolHandler的实例对象,这个实例属于哪个类由protocolHandlerClassName决定,这儿点开setProtocol方法:
public void setProtocol(String protocol) {
if (AprLifecycleListener.isAprAvailable()) {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11AprProtocol");
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.ajp.AjpAprProtocol");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol);
} else {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11AprProtocol");
}
} else {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName