Tomcat源碼分析-線程池應用

          記得前兩年剛去公司實習那會兒,從老大學習的第一個終身受益技能是,分析錯誤堆棧和線程調用棧,正是這點,發現tomcat啓動後線程數只有6個到第一次訪問後新增了10個線程數這個變化讓我很迷惑,於是開始了我的答疑解惑之旅。
         1、監聽8080端口接收請求線程的類是哪個?什麼時期啓動的?大概邏輯是什麼?
         大家都知道在發送http請求時,首先會進行tcp的三次握手,握手成功之後建立連接發送數據,因此該過程是在TCP協議對應抽象出來的類中,即Tomcat包(org.apache.tomcat.util.net)中的JIoEndpoint類。
         在初始化連接器(Connector)時,會最終初始化JIoEndpoint類,而該類的初始化其實就完成了監聽請求線程數的設置,最大連接數的設置,還有服務端ServerSocket的實例化,這些設置實則是通過其父類AbstractEndpoint在初始化時回調自己bind方法完成的。
         而該線程真正被創建是在連接器(Connector)啓動的過程中,連接器是在tomcat大多數組件啓動完後比較靠後才啓動的,連接器啓動最終會調用JIoEndpoint的startInternal方法,在在該方法中,會根據自身初始化時的設置信息實例化一個線程池,如該線程池已初始化則無需在實例化,之後在啓動監聽線程,啓動async timeout thread的線程。該線程池是存放處理具體任務線程的容器,該斷代碼如下
        
           
        2、數據是如何從接受線程移交到處理線程的?線程池是如何實現的?
         回答這個問題並不難,只是有些細節並不懂,想一探究竟。
         當有請求到來時,tomcat會對拿到socket進行一層封裝,封裝成SocketWrapper,之後會把SocketWrapper以組合的方式嵌入到任務SocketProcessor中,這樣以任務的形式就可以把數據帶到新的線程中處理,代碼如下:
         
        
         之後則是線程池具體是如何執行或者分配該任務的。
         首先計算線程池中的線程數是否小於核心線程數(corePoolSize),核心線程數是在實例化線程池對象時設置的,ThreadPoolExecutor中的execute代碼如下:
             
          如果當前線程數小於核心線程數(在tomcat中該值默認是10),則走addWorker邏輯 ;在addWorker方法中,首先會根據線程池狀態,任務對象,任務隊列這些因素做合法性檢查,檢查ok纔對線程池中已有線程數與核心線程數據做對比,小於纔會真正走後面產生線程,執行任務的邏輯,這部分代碼如下:
          
         break retry後又把任務封裝到一個新的任務中,與前面任務不同的是,該任務綁定了一個線程,並隨Worker的產生而產生,它與線程池中任務隊列中的任務是不同定位的,該對象的定位是線程池管理的線程,該對象數據結構如下:
        
         線程池通過一個HashSet<Worker>的數據結構存放已有的線程,因此線程池的池子可以指這個,新產生的線程會隨着Worker存到set中,代碼如下:
        
         
       進入到了addWorker這個方法,說明此時的任務隊列是空的,當時又出現了一個疑問,如下
       
        3、線程池開啓一個線程後,到底是在哪一步操作讓它處於等待狀態的,而不是直接結束掉?
         t.start()方法實則會開啓一個線程調用了Worker中的run方法,查看這段代碼不難發現,如當前處理的任務不爲空或者能從任務隊列中取到任務時,則在直接調用任務中的run方法進行真正業務處理,否則隊列就會阻塞,代碼如下:
         
        
        4、當線程池中的線程數已達到coreThreadNumber值時,任務隊列中又沒有任務,此時又來一個請求,請求是如何轉到其中一個線程處理的?
         這個問題的答案其實已經明確了,當任務隊列中有任務時,則會喚醒其中任意一個線程,如下是隊列實現阻塞與喚醒的邏輯。
         
       5、爲什麼在tomcat啓動後的第一次請求時常新增10個線程 ?
       這其實是在tomcat中的SocketProcessor(JIoEndpoint內部類)邏輯處理的,當socketwrapper被處理完後,狀態是OPEN,UPGRADING,UPGRADING_TOMCAT,UPGRADED狀態之一,則會繼續被包裝成一個SocketProcessor丟到線程池中進行處理,代碼如下:
       
         用360瀏覽器第一次訪問時會新增10個,而用edge瀏覽器訪問有時則新增2個或4個,其實都是根據上述邏輯來決定的。
         
        6、在HostConfig發佈同種類型的應用時也會用到線程池,爲何過一會兒這些線程消亡了,而第一次請求後創建的線程卻一直還存活着?
         其實查閱代碼後不難發現,在HostConfig裏面實例化線程池的時候多了一個設置,代碼如下:
         
     
       該屬性默認是false的,當該屬性爲是時,則從隊列中取值用的是非阻塞方法,此時線程不會阻塞,會執行完畢而結束掉。

        這個場景典型的體現生產者消費者模式設計,生產者指接收線程,它把接收到的請求封裝成特定的任務丟到線程池中,消費者指的是處理這些任務的線程,中間有一個存放任務的容器來協同兩者之間的工作。
       通過尋找這些問題的答案,其實就很清楚了線程池是如何工作的,以及tomcat中是如何運用線程池的,本篇線程池的內容是圍繞着這幾個問題來的,並未對線程池做完整邏輯做一一解讀。
       線程池裏面還是有很多內容的,比如通過用一個原子變量的高位表示狀態,低位表示可以擁有的最大容量,用了CAS的方式做自加等,網上有不少關於線程池的好文詳細分析過了,剩下的邏輯就不在在此贅述了。
       
       線程池相關文章
       ctl變量的解釋

       
 

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