Tomcat作为一个服务器来讲,必然运行着很多的线程,而每一个线程究竟是干什么的,这个需要非常的清楚,无论是打印断点,还是通过jstack进行线程栈分析,这都是必须要掌握的技能。 本文带你基于Tomcat7,8,9的版本,识别Tomcat堆栈中的线程。

 

1、main线程

main线程是tomcat的主要线程,其主要作用是通过启动包来对容器进行点火:

main线程一路启动了Catalina,StandardServer[8005],StandardService[Catalina],StandardEngine[Catalina]

​ engine内部组件都是异步启动,engine这层才开始继承ContainerBase,engine会调用父类的startInternal()方法,里面由startStopExecutor线程提交FutureTask任务,异步启动子组件StandardHost,

​ StandardEngine[Catalina].StandardHost[localhost]

main->Catalina->StandardServer->StandardService->StandardEngine->StandardHost,黑体开始都是异步启动。

​ ->启动Connector

main的作用就是把容器组件拉起来,然后阻塞在8005端口,等待关闭。

 

2、localhost-startStop线程

Tomcat容器被点火起来后,并不是傻傻的按照次序一步一步的启动,而是在engine组件中开始用该线程提交任务,按照层级进行异步启动,对于每一层级的组件都是采用startStop线程进行启动,我们观察一下idea中的线程堆栈就可以发现:启动异步,部署也是异步

1584196385034

这个startstop线程实际代码调用就是采用的JDK自带线程池来做的,启动位置就是ContainerBase的组件父类的startInternal():

1584196048806

因为从Engine开始往下的容器组件都是继承这个ContainerBase,所以相当于每一个组件启动的时候,除了对自身的状态进行设置,都会启动startChild线程启动自己的孩子组件。

而这个线程仅仅就是在启动时,当组件启动完成后,那么该线程就退出了,生命周期仅仅限于此。

3、AsyncFileHandlerWriter线程

日志输出线程:

1584196714178

顾名思义,该线程是用于异步文件处理的,它的作用是在Tomcat级别构架出一个输出框架,然后不同的日志系统都可以对接这个框架,因为日志对于服务器来说,是非常重要的功能。

如下,就是juli的配置:

1584196877893

该线程主要的作用是通过一个LinkedBlockingDeque来与log系统对接,该线程启动的时候就有了,全生命周期。

4、ContainerBackgroundProcessor线程

Tomcat在启动之后,不能说是死水一潭,很多时候可能会对Tomcat后端的容器组件做一些变化,例如部署一个应用,相当于你就需要在对应的Standardhost加上一个StandardContext,也有可能在热部署开关开启的时候,对资源进行增删等操作,这样应用可能会重新reload。

也有可能在生产模式下,对class进行重新替换等等,这个时候就需要在Tomcat级别中有一个线程能实时扫描Tomcat容器的变化,这个就是ContainerbackgroundProcessor线程了:

(本地源码StandardContext类的5212行启动)

1584200549363

我们可以看到这个代码,也就是在ContainerBase中:

1584200704730

这个线程是一个递归调用,也就是说,每一个容器组件其实都有一个backgroundProcessor,而整个Tomcat就点起一个线程开启扫描,扫完儿子,再扫孙子(实际上来说,主要还是用于StandardContext这一级,可以看到StandardContext这一级:

1584201033048

1584201068001

我们可以看到,每一次backgroundProcessor,都会对该应用进行一次全方位的扫描,这个时候,当你开启了热部署的开关,一旦class和资源发生变化,立刻就会reload。

tomcat9中已经被Catalina-Utility线程替代。

5、acceptor线程

Connector(实际是在AbstractProtocol类中)初始化和启动之时,启动了Endpoint,Endpoint就会启动poller线程和Acceptor线程。Acceptor底层就是ServerSocket.accept()。返回Socket之后丢给NioChannel处理,之后通道和poller线程绑定。

acceptor->poller->exec

无论是NIO还是BIO通道,都会有Acceptor线程,该线程就是进行socket接收的,它不会继续处理,如果是NIO的,无论是新接收的包还是继续发送的包,直接就会交给Poller,而BIO模式,Acceptor线程直接把活就给工作线程了:

1584238520638

如果不配置,Acceptor线程默认开始就开启1个,后期再随着压力增大而增长:

1584238658710

上述启动代码在AbstractNioEndpoint的startAcceptorThreads方法中。

6、ClientPoller线程

NIO和APR模式下的Tomcat前端,都会有Poller线程:

1584238019629

对于Poller线程实际就是继续接着Acceptor进行处理,展开Selector,然后遍历key,将后续的任务转交给工作线程(exec线程),起到的是一个缓冲,转接,和NIO事件遍历的作用,具体代码体现如下(NioEndpoint类):

1584238272139

上述的代码在NioEndpoint的startInternal中,默认开始开启2个Poller线程,后期再随着压力增大增长,可以在Connector中进行配置。

7、exe线程(默认10个)

也就是SocketProcessor线程,我们可以看到,上述几个线程都是定义在NioEndpoint内部线程类。NIO模式下,Poller线程将解析好的socket交给SocketProcessor处理,它主要是http协议分析,攒出Response和Request,然后调用Tomcat后端的容器:

1584239277914

1584239091296

该线程的重要性不言而喻,Tomcat主要的时间都耗在这个线程上,所以我们可以看到Tomcat里面有很多的优化,配置,都是基于这个线程的,尽可能让这个线程减少阻塞,减少线程切换,甚至少创建,多利用。

下面就是NIO模式下创建的工作线程:

 

实际上也是JDK的线程池,只不过基于Tomcat的不同环境参数,对JDK线程池进行了定制化而已,本质上还是JDK的线程池。

8、NioBlockingSelector.BlockPoller(默认2个)

Nio方式的Servlet阻塞输入输出检测线程。实际就是在Endpoint初始化的时候启动selectorPool,selectorPool再启动selector,selector内部启动BlokerPoller线程。

1584242888071

该线程在前面的NioBlockingPool中讲得很清楚了,其NIO通道的Servlet输入和输出最终都是通过NioBlockingPool来完成的,而NioBlockingPool又根据Tomcat的场景可以分成阻塞或者是非阻塞的,对于阻塞来讲,为了等待网络发出,需要启动一个线程实时监测网络socketChannel是否可以发出包,而如果不这么做的话,就需要使用一个while空转,这样会让工作线程一直损耗。

只要是阻塞模式,并且在Tomcat启动的时候,添加了—D参数 org.apache.tomcat.util.net.NioSelectorShared 的话,那么就会启动这个线程。

大体上启动顺序如下:

//bind方法在初始化就完成了
Endpoint.bind(){
    //selector池子启动
    selectorPool.open(){
        //池子里面selector再启动
         blockingSelector.open(getSharedSelector()){
             //重点这句
              poller = new BlockPoller();
              poller.selector = sharedSelector;
              poller.setDaemon(true);
              poller.setName("NioBlockingSelector.BlockPoller-"+       (threadCounter.getAndIncrement()));
             //这里启动
              poller.start();
         }
    }
}

9、AsyncTimeout线程

该线程为tomcat7及之后的版本才出现的,注释其实很清楚,该线程就是检测异步request请求时,触发超时,并将该请求再转发到工作线程池处理(也就是Endpoint处理)。

1584243575318

AsyncTimeout线程也是定义在AbstractProtocol内部的,在start()中启动。AbstractProtocol是个极其重要的类,他持有EndpointConnectionHandler这两个tomcat前端非常重要的类

1584243290669

10、其他线程(例如ajp相关线程)

ajp工作线程处理的是ajp协议的相关请求,这个请求主要是用于http apache服务器和tomcat之间的数据交换,该数据交换用的就是ajp协议,和exec工作线程差不多,默认也是启动10个,端口号是8009。优化时如果没有用到http apache的话就可以把这个协议关掉。

Tomcat本身还有很多其它的线程,远远不止这些,例如如果开启了sendfile,那么对sendfile就是开启一个线程来进行操作,这种功能的线程开启还有很多。

Tomcat作为一款优秀的服务器,不可能就只有1个线程,而是多个线程之间相互配合完成功能,而且很多功能尽量异步处理,尽可能的减少线程切换。所以线程并不是越多越好,因此线程的控制也尤为关键。

以上线程的源码分析详细讲解请查看以下文档

https://smartan123.github.io/book/?file=001-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/002-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/0021-tomcat%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐