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中的線程堆棧就可以發現:啓動異步,部署也是異步
這個startstop線程實際代碼調用就是採用的JDK自帶線程池來做的,啓動位置就是ContainerBase的組件父類的startInternal():
因爲從Engine開始往下的容器組件都是繼承這個ContainerBase,所以相當於每一個組件啓動的時候,除了對自身的狀態進行設置,都會啓動startChild線程啓動自己的孩子組件。
而這個線程僅僅就是在啓動時,當組件啓動完成後,那麼該線程就退出了,生命週期僅僅限於此。
3、AsyncFileHandlerWriter線程
日誌輸出線程:
顧名思義,該線程是用於異步文件處理的,它的作用是在Tomcat級別構架出一個輸出框架,然後不同的日誌系統都可以對接這個框架,因爲日誌對於服務器來說,是非常重要的功能。
如下,就是juli的配置:
該線程主要的作用是通過一個LinkedBlockingDeque來與log系統對接,該線程啓動的時候就有了,全生命週期。
4、ContainerBackgroundProcessor線程
Tomcat在啓動之後,不能說是死水一潭,很多時候可能會對Tomcat後端的容器組件做一些變化,例如部署一個應用,相當於你就需要在對應的Standardhost加上一個StandardContext,也有可能在熱部署開關開啓的時候,對資源進行增刪等操作,這樣應用可能會重新reload。
也有可能在生產模式下,對class進行重新替換等等,這個時候就需要在Tomcat級別中有一個線程能實時掃描Tomcat容器的變化,這個就是ContainerbackgroundProcessor線程了:
(本地源碼StandardContext類的5212行啓動)
我們可以看到這個代碼,也就是在ContainerBase中:
這個線程是一個遞歸調用,也就是說,每一個容器組件其實都有一個backgroundProcessor,而整個Tomcat就點起一個線程開啓掃描,掃完兒子,再掃孫子(實際上來說,主要還是用於StandardContext這一級,可以看到StandardContext這一級:
我們可以看到,每一次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線程直接把活就給工作線程了:
如果不配置,Acceptor線程默認開始就開啓1個,後期再隨着壓力增大而增長:
上述啓動代碼在AbstractNioEndpoint的startAcceptorThreads方法中。
6、ClientPoller線程
NIO和APR模式下的Tomcat前端,都會有Poller線程:
對於Poller線程實際就是繼續接着Acceptor進行處理,展開Selector,然後遍歷key,將後續的任務轉交給工作線程(exec線程),起到的是一個緩衝,轉接,和NIO事件遍歷的作用,具體代碼體現如下(NioEndpoint類):
上述的代碼在NioEndpoint的startInternal中,默認開始開啓2個Poller線程,後期再隨着壓力增大增長,可以在Connector中進行配置。
7、exe線程(默認10個)
也就是SocketProcessor線程,我們可以看到,上述幾個線程都是定義在NioEndpoint內部線程類。NIO模式下,Poller線程將解析好的socket交給SocketProcessor處理,它主要是http協議分析,攢出Response和Request,然後調用Tomcat後端的容器:
該線程的重要性不言而喻,Tomcat主要的時間都耗在這個線程上,所以我們可以看到Tomcat裏面有很多的優化,配置,都是基於這個線程的,儘可能讓這個線程減少阻塞,減少線程切換,甚至少創建,多利用。
下面就是NIO模式下創建的工作線程:
實際上也是JDK的線程池,只不過基於Tomcat的不同環境參數,對JDK線程池進行了定製化而已,本質上還是JDK的線程池。
8、NioBlockingSelector.BlockPoller(默認2個)
Nio方式的Servlet阻塞輸入輸出檢測線程。實際就是在Endpoint初始化的時候啓動selectorPool,selectorPool再啓動selector,selector內部啓動BlokerPoller線程。
該線程在前面的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處理)。
AsyncTimeout線程也是定義在AbstractProtocol內部的,在start()中啓動。AbstractProtocol是個極其重要的類,他持有Endpoint和ConnectionHandler這兩個tomcat前端非常重要的類
10、其他線程(例如ajp相關線程)
ajp工作線程處理的是ajp協議的相關請求,這個請求主要是用於http apache服務器和tomcat之間的數據交換,該數據交換用的就是ajp協議,和exec工作線程差不多,默認也是啓動10個,端口號是8009。優化時如果沒有用到http apache的話就可以把這個協議關掉。
Tomcat本身還有很多其它的線程,遠遠不止這些,例如如果開啓了sendfile,那麼對sendfile就是開啓一個線程來進行操作,這種功能的線程開啓還有很多。
Tomcat作爲一款優秀的服務器,不可能就只有1個線程,而是多個線程之間相互配合完成功能,而且很多功能儘量異步處理,儘可能的減少線程切換。所以線程並不是越多越好,因此線程的控制也尤爲關鍵。
以上線程的源碼分析詳細講解請查看以下文檔