執行緒池的介紹及簡單實現
關於執行緒池的簡單介紹
執行緒池的技術背景
在物件導向程式設計中,建立和銷燬物件是很費時間的,因為建立一個物件要獲取記憶體資源或者其它更多資源。在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而在處理請求時浪費時間,從而提高效率。
這些都是假設,不能充分說明問題,下面我將討論執行緒池的簡單實現並對該程式進行對比測試,以說明執行緒技術優點及應用領域。
執行緒池的簡單實現及對比測試
一般一個簡單執行緒池至少包含下列組成部分。
- 執行緒池管理器(ThreadPoolManager):用於建立並管理執行緒池
- 工作執行緒(WorkThread): 執行緒池中執行緒
- 任務介面(Task):每個任務必須實現的介面,以供工作執行緒排程任務的執行。
- 任務佇列:用於存放沒有處理的任務。提供一種緩衝機制。
執行緒池管理器至少有下列功能:建立執行緒池,銷燬執行緒池,新增新任務 建立執行緒池的部分程式碼如下:
… //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),機器配置在此不便指明。
測試資料及對應結果
執行緒池尺寸 | 任務數 | 沒有應用執行緒池所用的時間(單位:毫秒,OS:win) | 應用執行緒池所用的時間(單位:毫秒,OS:win) | 沒有應用執行緒池所用的時間(單位:毫秒,OS:Solaris) | 應用執行緒池所用的時間(單位:毫秒,OS:Solaris) |
1 | 5000 | 3896 | 130 | 6513 | 327 |
2 | 5000 | 3455 | 151 | 6221 | 659 |
4 | 5000 | 3425 | 120 | 5448 | 433 |
8 | 5000 | 3475 | 160 | 5769 | 1478 |
16 | 5000 | 3505 | 211 | 5785 | 1970 |
32 | 5000 | 3455 | 251 | 6403 | 875 |
64 | 5000 | 3595 | 501 | 5182 | 1103 |
128 | 5000 | 3515 | 881 | 5154 | 405 |
256 | 5000 | 3495 | 3104 | 5502 | 1589 |
512 | 5000 | 3425 | 5488 | 5667 | 1262 |
16 | 1 | 20 | 0 | 22 | 3 |
16 | 2 | 20 | 20 | 21 | 13 |
16 | 4 | 20 | 10 | 27 | 10 |
16 | 8 | 20 | 20 | 22 | 24 |
16 | 16 | 30 | 20 | 29 | 48 |
16 | 32 | 40 | 20 | 46 | 108 |
16 | 64 | 60 | 20 | 72 | 199 |
16 | 128 | 110 | 20 | 148 | 335 |
16 | 256 | 201 | 20 | 252 | 132 |
16 | 512 | 411 | 40 | 522 | 382 |
16 | 1024 | 811 | 71 | 1233 | 610 |
16 | 2048 | 1552 | 80 | 2045 | 135 |
16 | 4096 | 2874 | 250 | 4828 | 787 |
圖1.執行緒池的尺寸的對伺服器程式的效能影
執行緒池的尺寸的對伺服器程式效能的影響
根據以上統計資料可得出下圖:
圖2.任務數對伺服器程式的衝擊
任務數對伺服器程式的衝擊
資料分析如下:
圖1是改變執行緒池尺寸對伺服器效能的影響,在該測試過程中,伺服器的要完成的任務數固定為為5000。從圖1中可以看出合理配置執行緒池尺寸對於大量任務處理的效率有非常明顯的提高,但是一旦尺寸選擇不合理(過大或過小)就會嚴重降低影響伺服器效能。理論上"過小"將出現任務不能及時處理的情況,但在圖表中顯示出某些小尺寸的執行緒池表現很好,這是因為測試驅動中有很多執行緒同步開銷,且這個開銷相對於完成單個任務的時間是不能忽略的。"過大"則會出現執行緒間同步開銷太大的問題,而且線上程間切換很耗CPU時間,在圖表顯示的很清楚。可見任何一個好技術,如果濫用都會造成災難性後果。
圖2是用不同數量的任務來衝擊伺服器程式,在該測試過程中,伺服器執行緒池尺寸固定為16。可以看出執行緒池在處理少量任務時的優勢不明顯。所以執行緒池技術有一定的適應範圍,關於適用範圍將在後面討論。但對於大量的任務的處理,執行緒池的優勢表現非常卓越,伺服器程式處理請求的時間雖然有波動,但是其平均值相對小多了。
值得注意的是測試方案中,統計任務的完成時間沒有包含了建立執行緒池的時間。在實際執行緒池工作時,即利用執行緒池處理任務時,建立執行緒池的時間是不必計算在內的。
由於測試驅動程式有很多同步程式碼,特別是等待執行緒執行完畢的同步(程式碼中為sleepToWait(long l)方法的呼叫),這些程式碼降低了程式碼執行效率,這是測試驅動一個缺點,但這個測試驅動可以說明執行緒池相對於簡單使用執行緒的優勢。
關於高階執行緒池的討論
簡單執行緒池存在一些問題,比如如果有大量的客戶要求伺服器為其服務,但由於執行緒池的工作執行緒是有限的,伺服器只能為部分客戶服務,其它客戶提交的任務,只能在任務佇列中等待處理。一些系統設計人員可能會不滿這種狀況,因為他們對伺服器程式的響應時間要求比較嚴格,所以在系統設計時可能會懷疑執行緒池技術的可行性,但是執行緒池有相應的解決方案。調整優化執行緒池尺寸是高階執行緒池要解決的一個問題。主要有下列解決方案:
方案一:動態增加工作執行緒
在一些高階執行緒池中一般提供一個可以動態改變的工作執行緒數目的功能,以適應突發性的請求。一旦請求變少了將逐步減少執行緒池中工作執行緒的數目。當然執行緒增加可以採用一種超前方式,即批量增加一批工作執行緒,而不是來一個請求才建立建立一個執行緒。批量建立是更加有效的方式。該方案還有應該限制執行緒池中工作執行緒數目的上限和下限。否則這種靈活的方式也就變成一種錯誤的方式或者災難,因為頻繁的建立執行緒或者短時間內產生大量的執行緒將會背離使用執行緒池原始初衷--減少建立執行緒的次數。
舉例:Jini中的TaskManager,就是一個精巧執行緒池管理器,它是動態增加工作執行緒的。SQL Server採用單程式(Single Process)多執行緒(Multi-Thread)的系統結構,1024個數量的執行緒池,動態執行緒分配,理論上限32767。
方案二:優化工作執行緒數目
如果不想線上程池應用複雜的策略來保證工作執行緒數滿足應用的要求,你就要根據統計學的原理來統計客戶的請求數目,比如高峰時段平均一秒鐘內有多少任務要求處理,並根據系統的承受能力及客戶的忍受能力來平衡估計一個合理的執行緒池尺寸。執行緒池的尺寸確實很難確定,所以有時乾脆用經驗值。
舉例:在MTS中執行緒池的尺寸固定為100。
方案三:一個伺服器提供多個執行緒池
在一些複雜的系統結構會採用這個方案。這樣可以根據不同任務或者任務優先順序來採用不同執行緒池處理。
舉例:COM+用到了多個執行緒池。
這三種方案各有優缺點。在不同應用中可能採用不同的方案或者乾脆組合這三種方案來解決實際問題。
執行緒池使用範圍及應注意問題
下面是我總結的一些執行緒池應用範圍,可能是不全面的。
執行緒池的應用範圍:
- 需要大量的執行緒來完成任務,且完成任務的時間比較短。 WEB伺服器完成網頁請求這樣的任務,使用執行緒池技術是非常合適的。因為單個任務小,而任務數量巨大,你可以想象一個熱門網站的點選次數。 但對於長時間的任務,比如一個Telnet連線請求,執行緒池的優點就不明顯了。因為Telnet會話時間比執行緒的建立時間大多了。
- 對效能要求苛刻的應用,比如要求伺服器迅速相應客戶請求。
- 接受突發性的大量請求,但不至於使伺服器因此產生大量執行緒的應用。突發性大量客戶請求,在沒有執行緒池情況下,將產生大量執行緒,雖然理論上大部分作業系統執行緒數目最大值不是問題,短時間內產生大量執行緒可能使記憶體到達極限,並出現"OutOfMemory"的錯誤。
結束語
本文只是簡單介紹執行緒池技術。可以看出執行緒池技術對於伺服器程式的效能改善是顯著的。執行緒池技術在伺服器領域有著廣泛的應用前景。希望這項技術能夠應用到您的多執行緒服務程式中。
參考資料
Danny Ayers 等:Professional Java Server Programming
Tarak Modi: Why Thread Pools are Important in Java
Brett Spell :Professional java Programming
Jini(TM) v1.2.1程式碼
Brian Goetz 的 Java 理論和實踐專欄
Microsoft Knowledge Base Article :Q282490
相關文章
- 執行緒池介紹執行緒
- 簡單的執行緒池執行緒
- 簡易執行緒池實現執行緒
- Android執行緒池使用介紹Android執行緒
- 簡單的執行緒池(八)執行緒
- 簡單的執行緒池(二)執行緒
- 簡單的執行緒池(七)執行緒
- 簡單的執行緒池(四)執行緒
- 簡單的執行緒池(六)執行緒
- 簡單的執行緒池(三)執行緒
- 簡單的執行緒池(九)執行緒
- 【多執行緒系列】CAS、AQS簡單介紹執行緒AQS
- 執行緒池的實現執行緒
- 透過簡單示例瞭解執行緒池實現原理執行緒
- 簡單C++執行緒池C++執行緒
- SpringBoot執行緒池和Java執行緒池的實現原理Spring Boot執行緒Java
- 達夢資料庫執行緒簡單介紹資料庫執行緒
- python執行緒池的實現Python執行緒
- 執行緒池的實現原理執行緒
- 執行緒池其實看懂了也很簡單執行緒
- 執行緒介紹及建立方式執行緒
- Linux雜談: 實現一種簡單實用的執行緒池(C語言)Linux執行緒C語言
- Python簡單實現多執行緒例子Python執行緒
- 執行緒池的實現原始碼及應用舉例執行緒原始碼
- 執行緒池ThreadPoolExecutor實現原理執行緒thread
- epoll程式設計,單epoll+執行緒池?執行緒池+epoll?nginx實現高併發的原理?程式設計執行緒Nginx
- 【Java】【多執行緒】執行緒池簡述Java執行緒
- 執行緒池沒你想的那麼簡單(續)執行緒
- Python執行緒池 ThreadPoolExecutor 的用法及實戰Python執行緒thread
- 執行緒池的實現程式碼分析執行緒
- 詳解執行緒池的作用及Java中如何使用執行緒池執行緒Java
- 手寫執行緒池,對照學習ThreadPoolExecutor執行緒池實現原理!執行緒thread
- 執行緒簡介執行緒
- python實現自定義執行緒池Python執行緒
- 簡單介紹NMS的實現方法
- 執行緒池的五種狀態及建立執行緒池的幾種方式執行緒
- 執行緒池續:你必須要知道的執行緒池submit()實現原理之FutureTask!執行緒MIT
- Flownet 介紹 及光流的簡單介紹
- 多執行緒系列(十七) -執行緒組介紹執行緒