幾種開源Java Web容器線程池的實現方法簡介——Tomcat(二)
ThreadPool提供的僅僅是線程池的實現,而如何使用線程池也是有很大學問的。讓我們看看Tomcat是如何使用ThreadPool的吧。
Tomcat有兩種EndPoint,分別是AprEndpoint和PoolTcpEndpoint。前者自己實現了一套線程池(其實這和Tomcat 老版本的方案是相同的,至今Tomcat中還保留著老版本的線程池,PoolTcpEndpoint也有類似的代碼,通過“策略”可以選擇不同的線程池方案)。我們只關注PoolTcpEndpoint如何使用ThreadPool的。
首先,PoolTcpEndpoint建立了一個ThreadPoolRunnable執行個體——LeaderFollowerWorkerThread,實際上該執行個體就是接收(Accept)並處理(Process)使用者socket請求。接著將該執行個體放進ThreadPool中並運行,此時就可以接收使用者的請求了。
當有Socket請求時,LeaderFollowerWorkerThread首先獲得了Socket執行個體,注意此時 LeaderFollowerWorkerThread並沒有急著處理該Socket,而是在響應Socket訊息前,再次將 LeaderFollowerWorkerThread放進ThreadPool中,從而它(當然是另外一個線程了)可以繼續處理其他使用者的Socket 請求;接著,擁有Socket的LeaderFollowerWorkerThread再來處理該使用者的Socket請求。
整個過程與傳統的處理使用者Socket請求是不同的,也和Tomcat老版本不同。傳統的處理方法是:有一個後台啟動並執行監聽線程負責統一處理接收(注意只是“接收”)Socket請求,當有新的Socket請求時,將它賦值給一個Worker線程(通常是喚醒該線程),並有後者處理Socket請求,監聽線程繼續等待其他Socket請求。所以整個過程中有一個從Listener到Worker切換的過程。
而新版本Tomcat很有創造性的使用了另外一種方法,正如前文所描述的,接收和處理某個使用者Socket請求的始終是由一個線程全程負責,沒有切換到其他線程處理,少了這種線程間的切換是否更有效率呢?我還不能確認。不過這種使用方式確實有別於傳統模式,有種耳目一新的感覺。
幾種開源Java Web容器線程池的實現方法簡介——Jetty(三)
除了Tomcat外,Jetty是另外一個重要的Java Web容器,號稱“最小的”Web容器,從Jetty的原始碼規模可以看出它確實比較小。而且它的ThreadPool的實現也非常簡單,整個代碼ThreadPool代碼只有450行左右,可見小巧之極。
ThreadPool代碼位於com.mortbty.thread包中,其中最重要的方法是dispatch()和內部類PoolThread。顧名思義,dispatch方法主要是將Runnable執行個體派給線程池中的空閑PoolThread,由後者運行Runnable。
還是看看整個過程吧。首先,ThreadPool建立_minThreads個空閑PoolThread,並把它們添加到空閑線程隊列中。當需要運行 Runnable時,首先尋找是否有閒置PoolThread,如果有閒置,這由它處理;如果沒有並且PoolThread並沒有超過 _maxThreads個時,則建立一個新的PoolThread,並由這個新建立的PoolThread運行Runnable;如果 PoolThread超過了_maxThreads,則一直等待有閒置PoolThread出現。在PoolThread運行之前,必須把該 PoolThread從空閑線程隊列中移走。
再來看看PoolThread的實現吧。和所有的Worker線程一樣,用一個while(flag){wait();}迴圈等待Runnable的到來,當有Runnable被ThreadPool.dispatch()時,該PoolThread就運行Runnable;當運行完成後,再“歸還”給空閑線程隊列。
Jetty如何使用ThreadPool?整個Jetty只使用了一個ThreadPool執行個體,具體入口在 org.mortbay.jetty.Server中被執行個體化的,Connector中也使用Server的ThreadPool處理使用者的Socket 請求。Connector是處理使用者Socket請求的入口,一個Connector建立_acceptors個Acceptor,由Acceptor處理使用者Socket請求時,當有Socket請求時,就建立一個Connection放到線程池中處理,而Acceptor繼續處理其他的Socket請求。這是個傳統的Listener和Worker處理方式。
幾種開源Java Web容器線程池的實現方法簡介——Resin(四)
在這些Java Web容器中,Resin算得上很特別的,小巧穩定,而且效率很高。在這些Java Web容器中,算它的效率最高了。很多大型的網站中都能找到它的身影。Resin從3.0版本後開始走“特色”的開源路,與MySql很相似——如果用於商業目的,則需要買它的License。但對於個人研究而言,這已經不錯了,在網站上可以下載除了涉及License的原始碼外其他所有代碼。
說Resin特別,還主要是由於它的效能出眾,即使在很多企業級應用中也能派上用場。Resin的資料庫連接池做的很不錯,效率非常高。不過這裡我們討論它的線程池,看看有何特別之處。
Resin的ThreadPool位於com.caucho.util.ThreadPool中,不過這個類的命名有點蹊蹺,更恰當的命名是ThreadPoolItem,因為它確實只是一個普通的Thread。那線程調度何管理在哪裡呢?也在這個類中,不過都是以靜態函數方式提供的,所以這個類起到了兩重作用:線程池調度和Worker線程。也由於這種原因,Resin執行個體中只有一個線程池,不像Tomcat和Jetty可以同時運行多個線程池,不過對於一個系統而言,一個線程池足夠了。
和其他線程池實現方式不同的是,Resin採用鏈表儲存線程。如果有請求時,就將Head移走並喚醒該線程;待運行完成後,該線程就變成空閑狀態並且被添加到鏈表的Head部分。另外,每一個線程運行時都要判斷當前空閑線程數是否超過_minSpareThreads,如果超過了,該線程就會退出(狀態變成Dead),也從鏈表中刪除。
Resin如何使用該ThreadPool?所有需要用線程池的地方,只需調用ThreadPool.Schedule(Runnable)即可。該方法就是一個靜態函數,顧名思義,就是將Runnable加到ThreadPool中待運行。
Resin使用的還是傳統方法:監聽線程(com.caucho.server.port.Port),系統中可以有多個Port執行個體,前提連接埠號碼不同,比如有80和8080連接埠;另外就是Worker線程,其實就是ThreadPool中的空閑線程。Port本身是一個Thread,在啟動時,會在 ThreadPool中運行5個線程——TcpConnection同時等待使用者請求,當有使用者請求時,其中的一個會處理。其他繼續等待。當處理使用者請求完成後,還可以重用這些TcpConnection,這與Jetty的有所不同,Jetty是當有使用者請求時,才建立串連,處理完成後也不會重用這些串連,效率會稍差一些。
另外Resin有兩個後台運行線程:ThreadLauncher和ScheduleThread,前者負責當空閑線程小於最小空閑線程時建立新的線程;而後者則負責運行實際的Runnable。我覺得有的負責,沒有必要用一個線程來建立新線程,多此一舉。不過ScheduleThread是必須的,因為它就是Worker線程。
June 23rd, 2006
幾種開源Java Web容器線程池的實現方法簡介——總結(五)
介紹了tomcat、jetty和resin三種Java Web容器的線程池後,按照慣例應該比較它們的優缺點。不過先總結線程池的特點。
線程池作為提高程式處理資料能力的一種方案,應用非常廣泛。大量的伺服器都或多或少的使用到了線程池技術,不管是用Java還是C++實現,線程池都有如下的特點:
線程池一般有三個重要參數:
1. 最大線程數。在程式啟動並執行任何時候,線程數總數都不會超過這個數。如果請求數量超過最大數時,則會等待其他線程結束後再處理。
2. 最大共用線程數,即最大空閑線程數。如果當前的空閑線程數超過該值,則多餘的線程會被殺掉。
3. 最小共用線程數,即最小空閑線程數。如果當前的空閑數小於該值,則一次性建立這個數量的空閑線程,所以它本身也是一個建立線程的步長。
線程池有兩個概念:
1. Worker線程。背景工作執行緒主要是運行執行代碼,有兩種狀態:空閑狀態和運行狀態。在空閑狀態時,類似“休眠”,等待任務;處理運行狀態時,表示正在運行任務(Runnable)。
2. 輔助線程。主要負責監控線程池的狀態:空閑線程是否超過最大空閑線程數或者小於最小空閑線程數等。如果不滿足要求,就調整之。
如果按照上述標準去考察這三個容器就會發現:Tomcat實現的線程池是最完備的,Resin次之,而Jetty最為簡單。Jetty沒有控制空閑線程的數量,可能最後空閑線程數會達到最大線程數,影像效能,畢竟即使是休眠線程也會耗費CPU時鐘的。
談談Resin的線程池。Resin的實現比Tomcat複雜些。也有上述三個參數,也有兩個概念,這與Tomcat相當。但考慮到如何使用ThreadPool時,Resin也要複雜些。
或許由於Resin的ThreadPool是單間模式的,所有使用ThreadPool的線程都是相同設定,比如相同的最大線程數,最大空閑線程數等,在使用它時會多些考慮。比如在控制最大Socket串連數時,com.caucho.server.port.Port還要有自己的一套控制“數量”的機制,而無法使用ThreadPool所特有的控制機制。所以使用起來比Tomcat複雜。
Tomcat使用ThreadPool卻很簡單。由於Tomcat的ThreadPool可以有不同的執行個體存在,很方便的定製屬於自己的“數量”控制,直接用ThreadPool控制Socket串連數量。所以代碼也比較清爽。
如果要使用線程池,那就用Tomcat的ThreadPool吧。