tomcat堆棧中10大常見線程詳解

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章