不錯不錯
原文地址:幾種開源Java Web容器線程池的實現方法簡介
作者:吳越之地曉日初升
幾種開源Java Web容器線程池的實現方法簡介——Tomcat(一)
目前市場上常用的開源Java Web容器有Tomcat、Resin和Jetty。其中Resin從V3.0后需要購買才能用於商業目的,而其他兩種則是純開源的。可以分別從他們的網站上下載最新的二進制包和源代碼。
作為Web容器,需要承受較高的訪問量,能夠同時響應不同用戶的請求,能夠在惡劣環境下保持較高的穩定性和健壯性。在HTTP服務器領域,Apache HTTPD的效率是最高的,也是最為穩定的,但它只能處理靜態頁面的請求,如果需要支持動態頁面請求,則必須安裝相應的插件,比如mod_perl可以處理Perl腳本,mod_python可以處理Python腳本。
上面介紹的三中Web容器,都是使用Java編寫的HTTP服務器,當然他們都可以嵌到Apache中使用,也可以獨立使用。分析它們處理客戶請求的方法有助於了解Java多線程和線程池的實現方法,為設計強大的多線程服務器打好基礎。
Tomcat是使用最廣的Java Web容器,功能強大,可擴展性強。最新版本的Tomcat(5.5.17)為了提高響應速度和效率,使用了Apache Portable Runtime(APR)作為最底層,使用了APR中包含Socket、緩沖池等多種技術,性能也提高了。APR也是Apache HTTPD的最底層。可想而知,同屬於ASF(Apache Software Foundation)中的成員,互補互用的情況還是很多的,雖然使用了不同的開發語言。
Tomcat 的線程池位於tomcat-util.jar文件中,包含了兩種線程池方案。方案一:使用APR的Pool技術,使用了JNI;方案二:使用Java實現的ThreadPool。這里介紹的是第二種。如果想了解APR的Pool技術,可以查看APR的源代碼。
ThreadPool默認創建了5個線程,保存在一個200維的線程數組中,創建時就啟動了這些線程,當然在沒有請求時,它們都處理“等待”狀態(其實就是一個while循環,不停的等待notify)。如果有請求時,空閑線程會被喚醒執行用戶的請求。
具體的請求過程是:服務啟動時,創建一個一維線程數組(maxThread=200個),並創建空閑線程(minSpareThreads=5個)隨時等待用戶請求。當有用戶請求時,調用 threadpool.runIt(ThreadPoolRunnable)方法,將一個需要執行的實例傳給ThreadPool中。其中用戶需要執行的實例必須實現ThreadPoolRunnable接口。 ThreadPool首先查找空閑的線程,如果有則用它運行要執行ThreadPoolRunnable;如果沒有空閑線程並且沒有超過maxThreads,就一次性創建minSpareThreads個空閑線程;如果已經超過了maxThreads了,就等待空閑線程了。總之,要找到空閑的線程,以便用它執行實例。找到后,將該線程從線程數組中移走。接着喚醒已經找到的空閑線程,用它運行執行實例(ThreadPoolRunnable)。運行完ThreadPoolRunnable后,就將該線程重新放到線程數組中,作為空閑線程供后續使用。
由此可以看出,Tomcat的線程池實現是比較簡單的,ThreadPool.java也只有840行代碼。用一個一維數組保存空閑的線程,每次以一個較小步伐(5個)創建空閑線程並放到線程池中。使用時從數組中移走空閑的線程,用完后,再“歸還”給線程池。
June 19th, 2006
幾種開源Java Web容器線程池的實現方法簡介——Tomcat(二)
ThreadPool提供的僅僅是線程池的實現,而如何使用線程池也是有很大學問的。讓我們看看Tomcat是如何使用ThreadPool的吧。
幾種開源Java Web容器線程池的實現方法簡介——Tomcat(二)
ThreadPool提供的僅僅是線程池的實現,而如何使用線程池也是有很大學問的。讓我們看看Tomcat是如何使用ThreadPool的吧。
Tomcat有兩種EndPoint,分別是AprEndpoint和PoolTcpEndpoint。前者自己實現了一套線程池(其實這和Tomcat老版本的方案是相同的,至今Tomcat中還保留着老版本的線程池,PoolTcpEndpoint也有類似的代碼,通過“策略”可以選擇不同的線程池方案)。我們只關注PoolTcpEndpoint如何使用ThreadPool的。
首先,PoolTcpEndpoint創建了一個ThreadPoolRunnable實例——LeaderFollowerWorkerThre
ad,實際上該實例就是接收(Accept)並處理(Process)用戶socket請求。接着將該實例放進ThreadPool中並運行,此時就可以接收用戶的請求了。
當有Socket請求時,LeaderFollowerWorkerThre
ad首先獲得了Socket實例,注意此時LeaderFollowerWorkerThre
ad並沒有急着處理該Socket,而是在響應Socket消息前,再次將LeaderFollowerWorkerThre
ad放進ThreadPool中,從而它(當然是另外一個線程了)可以繼續處理其他用戶的Socket請求;接着,擁有Socket的LeaderFollowerWorkerThre
ad再來處理該用戶的Socket請求。
整個過程與傳統的處理用戶Socket請求是不同的,也和Tomcat老版本不同。傳統的處理方法是:有一個后台運行的監聽線程負責統一處理接收(注意只是“接收”)Socket請求,當有新的Socket請求時,將它賦值給一個Worker線程(通常是喚醒該線程),並有后者處理Socket請求,監聽線程繼續等待其他Socket請求。所以整個過程中有一個從Listener到Worker切換的過程。
而新版本Tomcat很有創造性的使用了另外一種方法,正如前文所描述的,接收和處理某個用戶Socket請求的始終是由一個線程全程負責,沒有切換到其他線程處理,少了這種線程間的切換是否更有效率呢?我還不能確認。不過這種使用方式確實有別於傳統模式,有種耳目一新的感覺。
June 20th, 2006
幾種開源Java Web容器線程池的實現方法簡介——Jetty(三)
除了Tomcat外,Jetty是另外一個重要的Java Web容器,號稱“最小的”Web容器,從Jetty的源代碼規模可以看出它確實比較小。而且它的ThreadPool的實現也非常簡單,整個代碼ThreadPool代碼只有450行左右,可見小巧之極。
幾種開源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處理方式。
June 21st, 2006
幾種開源Java Web容器線程池的實現方法簡介——Resin(四)
在這些Java Web容器中,Resin算得上很特別的,小巧穩定,而且效率很高。在這些Java Web容器中,算它的效率最高了。很多大型的網站中都能找到它的身影。Resin從3.0版本后開始走“特色”的開源路,與MySql很相似——如果用於商業目的,則需要買它的License。但對於個人研究而言,這已經不錯了,在網站上可以下載除了涉及License的源代碼外其他所有代碼。
幾種開源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 Web容器線程池的實現方法簡介——總結(五)
介紹了tomcat、jetty和resin三種Java Web容器的線程池后,按照慣例應該比較它們的優缺點。不過先總結線程池的特點。
線程池作為提高程序處理數據能力的一種方案,應用非常廣泛。大量的服務器都或多或少的使用到了線程池技術,不管是用Java還是C++實現,線程池都有如下的特點:
線程池一般有三個重要參數:
1. 最大線程數。在程序運行的任何時候,線程數總數都不會超過這個數。如果請求數量超過最大數時,則會等待其他線程結束后再處理。
2. 最大共享線程數,即最大空閑線程數。如果當前的空閑線程數超過該值,則多余的線程會被殺掉。
3. 最小共享線程數,即最小空閑線程數。如果當前的空閑數小於該值,則一次性創建這個數量的空閑線程,所以它本身也是一個創建線程的步長。
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吧。