服務器程序利用線程技術響應客戶請求已經司空見慣,可能您認為這樣做效率已經很高,但您有沒有想過優化一下使用線程的方法。該文章將向您介紹服務器程序如何利用線程池來優化性能並提供一個簡單的線程池實現。
在面向對象編程中,創建和銷毀對象是很費時間的,因為創建一個對象要獲取內存資源或者其它更多資源。在Java中更是如此,虛擬機將試圖跟蹤每一個對象,以便能夠在對象銷毀後進行垃圾回收。所以提高服務程序效率的一個手段就是盡可能減少創建和銷毀對象的次數,特別是一些很耗資源的對象創建和銷毀。如何利用已有對象來服務就是一個需要解決的關鍵問題,其實這就是一些"池化資源"技術產生的原因。比如大家所熟悉的數據庫連接池正是遵循這一思想而產生的,本文將介紹的線程池技術同樣符合這一思想。
目前,一些著名的大公司都特別看好這項技術,並早已經在他們的產品中應用該技術。比如IBM的WebSphere,IONA的Orbix 2000在SUN的 Jini中,Microsoft的MTS(Microsoft Transaction Server 2.0),COM+等。
現在您是否也想在服務器程序應用該項技術?
我所提到服務器程序是指能夠接受客戶請求並能處理請求的程序,而不只是指那些接受網絡客戶請求的網絡服務器程序。
多線程技術主要解決處理器單元內多個線程執行的問題,它可以顯著減少處理器單元的閒置時間,增加處理器單元的吞吐能力。但如果對多線程應用不當,會增加對單個任務的處理時間。可以舉一個簡單的例子:
假設在一台服務器完成一項任務的時間為T
T1 創建線程的時間 T2 在線程中執行任務的時間,包括線程間同步所需時間 T3 線程銷毀的時間
顯然T = T1+T2+T3。注意這是一個極度簡化的假設。
可以看出T1,T3是多線程本身的帶來的開銷,我們渴望減少T1,T3所用的時間,從而減少T的時間。但一些線程的使用者並沒有注意到這一點,所以在程序中頻繁的創建或銷毀線程,這導致T1和T3在T中占有相當比例。顯然這是突出了線程的弱點(T1,T3),而不是優點(並發性)。
線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高服務器程序性能的。它把T1,T3分別安排在服務器程序的啟動和結束的時間段或者一些空閒的時間段,這樣在服務器程序處理客戶請求時,不會有T1,T3的開銷了。
線程池不僅調整T1,T3產生的時間段,而且它還顯著減少了創建線程的數目。在看一個例子:
假設一個服務器一天要處理50000個請求,並且每個請求需要一個單獨的線程完成。我們比較利用線程池技術和不利於線程池技術的服務器處理這些請求時所產生的線程總數。在線程池中,線程數一般是固定的,所以產生線程總數不會超過線程池中線程的數目或者上限(以下簡稱線程池尺寸),而如果服務器不利用線程池來處理這些請求則線程總數為50000。一般線程池尺寸是遠小於50000。所以利用線程池的服務器程序不會為了創建50000而在處理請求時浪費時間,從而提高效率。
這些都是假設,不能充分說明問題,下面我將討論線程池的簡單實現並對該程序進行對比測試,以說明線程技術優點及應用領域。
一般一個簡單線程池至少包含下列組成部分。
線程池管理器至少有下列功能:創建線程池,銷毀線程池,添加新任務 創建線程池的部分代碼如下:
… //create threads synchronized(workThreadVector) { for(int j = 0; j < i; j++) { threadNum++; WorkThread workThread = new WorkThread(taskVector, threadNum); workThreadVector.addElement(workThread); } } …
注意同步workThreadVector並沒有降低效率,相反提高了效率,請參考Brian Goetz的文章。 銷毀線程池的部分代碼如下:
… while(!workThreadVector.isEmpty()) { if(debugLevel > 2) System.out.println("stop:"+(i)); i++; try { WorkThread workThread = (WorkThread)workThreadVector.remove(0); workThread.closeThread(); continue; } catch(Exception exception) { if(debugLevel > 2) exception.printStackTrace(); } break; } …
添加新任務的部分代碼如下:
… synchronized(taskVector) { taskVector.addElement(taskObj); taskVector.notifyAll(); } …
工作線程是一個可以循環執行任務的線程,在沒有任務時將等待。由於代碼比較多在此不羅列.
任務接口是為所有任務提供統一的接口,以便工作線程處理。任務接口主要規定了任務的入口,任務執行完後的收尾工作,任務的執行狀態等。在文章結尾有相關代碼的下載。
以上所描述的線程池結構很簡單,一些復雜的線程池結構將不再此討論。
在下載代碼中有測試驅動程序(TestThreadPool),我利用這個測試程序的輸出數據統計出下列測試結果。測試有兩個參數要設置:
分別將一個參數固定,另一個參數變動以考察兩個參數所產生的不同結果。所用測試機器分別為普通PC機(Win2000 JDK1.3.1)和SUN服務器(Solaris Unix JDK1.3.1),機器配置在此不便指明。
根據以上統計數據可得出下圖:
數據分析如下:
圖1是改變線程池尺寸對服務器性能的影響,在該測試過程中,服務器的要完成的任務數固定為為5000。從圖1中可以看出合理配置線程池尺寸對於大量任務處理的效率有非常明顯的提高,但是一旦尺寸選擇不合理(過大或過小)就會嚴重降低影響服務器性能。理論上"過小"將出現任務不能及時處理的情況,但在圖表中顯示出某些小尺寸的線程池表現很好,這是因為測試驅動中有很多線程同步開銷,且這個開銷相對於完成單個任務的時間是不能忽略的。"過大"則會出現線程間同步開銷太大的問題,而且在線程間切換很耗CPU時間,在圖表顯示的很清楚。可見任何一個好技術,如果濫用都會造成災難性後果。
圖2是用不同數量的任務來沖擊服務器程序,在該測試過程中,服務器線程池尺寸固定為16。可以看出線程池在處理少量任務時的優勢不明顯。所以線程池技術有一定的適應范圍,關於適用范圍將在後面討論。但對於大量的任務的處理,線程池的優勢表現非常卓越,服務器程序處理請求的時間雖然有波動,但是其平均值相對小多了。
值得注意的是測試方案中,統計任務的完成時間沒有包含了創建線程池的時間。在實際線程池工作時,即利用線程池處理任務時,創建線程池的時間是不必計算在內的。
由於測試驅動程��有很多同步代碼,特別是等待線程執行完畢的同步(代碼中為sleepToWait(long l)方法的調用),這些代碼降低了代碼執行效率,這是測試驅動一個缺點,但這個測試驅動可以說明線程池相對於簡單使用線程的優勢。
更多詳情見請繼續閱讀下一頁的精彩內容: http://www.linuxidc.com/Linux/2015-02/113706p2.htm