100行Java程式碼構建一個執行緒池

pingyuan發表於2007-05-25
100行Java程式碼構建一個執行緒池[@more@]在現代的作業系統中,有一個很重要的概念――執行緒,幾乎所有目前流行的作業系統都支援執行緒,執行緒來源於作業系統中程式的概念,程式有自己的虛擬地址空間以及正文段、資料段及堆疊,而且各自佔有不同的系統資源(例如檔案、環境變數等等)。與此不同,執行緒不能單獨存在,它依附於程式,只能由程式派生。如果一個程式派生出了兩個執行緒,那這兩個執行緒共享此程式的全域性變數和程式碼段,但每個執行緒各擁有各自的堆疊,因此它們擁有各自的區域性變數,執行緒在UNIX系統中還被進一步分為使用者級執行緒(由程式自已來管理)和系統級執行緒(由作業系統的排程程式來管理)。

  既然有了程式,為什麼還要提出執行緒的概念呢?因為與建立一個新的程式相比,建立一個執行緒將會耗費小得多的系統資源,對於一些小型的應用,可能感覺不到這點,但對於那些併發程式數特別多的應用,使用執行緒會比使用程式獲得更好的效能,從而降低作業系統的負擔。另外,執行緒共享建立它的程式的全域性變數,因此執行緒間的通訊程式設計會更將簡單,完全可以拋棄傳統的程式間通訊的IPC程式設計,而採用共享全域性變數來進行執行緒間通訊。

  有了上面這個概念,我們下面就進入正題,來看一下執行緒池究竟是怎麼一回事?其實執行緒池的原理很簡單,類似於作業系統中的緩衝區的概念,它的流程如下:先啟動若干數量的執行緒,並讓這些執行緒都處於睡眠狀態,當客戶端有一個新請求時,就會喚醒執行緒池中的某一個睡眠執行緒,讓它來處理客戶端的這個請求,當處理完這個請求後,執行緒又處於睡眠狀態。可能你也許會問:為什麼要搞得這麼麻煩,如果每當客戶端有新的請求時,我就建立一個新的執行緒不就完了?這也許是個不錯的方法,因為它能使得你編寫程式碼相對容易一些,但你卻忽略了一個重要的問題――效能!就拿我所在的單位來說,我的單位是一個省級資料大集中的銀行網路中心,高峰期每秒的客戶端請求併發數超過100,如果為每個客戶端請求建立一個新執行緒的話,那耗費的CPU時間和記憶體將是驚人的,如果採用一個擁有200個執行緒的執行緒池,那將會節約大量的的系統資源,使得更多的CPU時間和記憶體用來處理實際的商業應用,而不是頻繁的執行緒建立與銷燬。

  既然一切都明白了,那我們就開始著手實現一個真正的執行緒池吧,執行緒程式設計可以有多種語言來實現,例如C、C++、java等等,但不同的作業系統提供不同的執行緒API介面,為了讓你能更明白執行緒池的原理而避免陷入煩瑣的API呼叫之中,我採用了JAVA語言來實現它,由於JAVA語言是一種跨平臺的語言,因此你不必為使用不同的作業系統而無法編譯執行本程式而苦惱,只要你安裝了JDK1.2以上的版本,都能正確地編譯執行本程式。另外JAVA語言本身就內建了執行緒物件,而且JAVA語言是完全面像物件的,因此能夠讓你更清晰地瞭解執行緒池的原理,如果你注意看一下本文的標題,你會發現整個示例程式的程式碼只有大約100行。

  本示例程式由三個類構成,第一個是TestThreadPool類,它是一個測試程式,用來模擬客戶端的請求,當你執行它時,系統首先會顯示執行緒池的初始化資訊,然後提示你從鍵盤上輸入字串,並按下Enter鍵,這時你會發現螢幕上顯示資訊,告訴你某個執行緒正在處理你的請求,如果你快速地輸入一行行字串,那麼你會發現執行緒池中不斷有執行緒被喚醒,來處理你的請求,在本例中,我建立了一個擁有10個執行緒的執行緒池,如果執行緒池中沒有可用執行緒了,系統會提示你相應的警告資訊,但如果你稍等片刻,那你會發現螢幕上會陸陸續續提示有執行緒進入了睡眠狀態,這時你又可以傳送新的請求了。

  第二個類是ThreadPoolManager類,顧名思義,它是一個用於管理執行緒池的類,它的主要職責是初始化執行緒池,併為客戶端的請求分配不同的執行緒來進行處理,如果執行緒池滿了,它會對你發出警告資訊。

  最後一個類是SimpleThread類,它是Thread類的一個子類,它才真正對客戶端的請求進行處理,SimpleThread在示例程式初始化時都處於睡眠狀態,但如果它接受到了ThreadPoolManager類發過來的排程資訊,則會將自己喚醒,並對請求進行處理。


  首先我們來看一下TestThreadPool類的原始碼:



//TestThreadPool.java
1 import java.io.*;
2
3
4 public class TestThreadPool
5 {
6 public static void main(String[] args)
7 {
8 try{
9 BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
10 String s;
11 ThreadPoolManager manager = new ThreadPoolManager(10);
12 while((s = br.readLine()) != null)
13 {
14 manager.process(s);
15 }
16 }catch(IOException e){}
17 }
18 }



  由於此測試程式用到了輸入輸入類,因此第1行匯入了JAVA的基本IO處理包,在第11行中,我們建立了一個名為manager的類,它給ThreadPoolManager類的建構函式傳遞了一個值為10的引數,告訴ThreadPoolManager類:我要一個有10個執行緒的池,給我建立一個吧!第12行至15行是一個無限迴圈,它用來等待使用者的鍵入,並將鍵入的字串儲存在s變數中,並呼叫ThreadPoolManager類的process方法來將這個請求進行處理。

  下面我們再進一步跟蹤到ThreadPoolManager類中去,以下是它的原始碼:



//ThreadPoolManager.java
1 import java.util.*;
2
3
4 class ThreadPoolManager
5 {
6
7 private int maxThread;
8 public Vector vector;
9 public void setMaxThread(int threadCount)
10 {
11 maxThread = threadCount;
12 }
13
14 public ThreadPoolManager(int threadCount)
15 {
16 setMaxThread(threadCount);
17 System.out.println("Starting thread pool...");
18 vector = new Vector();
19 for(int i = 1; i <= 10; i++)
20 {
21 SimpleThread thread = new SimpleThread(i);
22 vector.addElement(thread);
23 thread.start();
24 }
25 }
26
27 public void process(String argument)
28 {
29 int i;
30 for(i = 0; i < vector.size(); i++)
31 {
32 SimpleThread currentThread = (SimpleThread)vector.elementAt(i);
33 if(!currentThread.isRunning())
34 {
35 System.out.println("Thread "+ (i+1) +" is processing:" +
argument);
36 currentThread.setArgument(argument);
37 currentThread.setRunning(true);
38 return;
39 }
40 }
41 if(i == vector.size())
42 {
43 System.out.println("pool is full, try in another time.");
44 }
45 }
46 }//end of class ThreadPoolManager



  我們先關注一下這個類的建構函式,然後再看它的process()方法。第16-24行是它的建構函式,首先它給ThreadPoolManager類的成員變數maxThread賦值,maxThread表示用於控制執行緒池中最大執行緒的數量。第18行初始化一個陣列vector,它用來存放所有的SimpleThread類,這時候就充分體現了JAVA語言的優越性與藝術性:如果你用C語言的話,至少要寫100行以上的程式碼來完成vector的功能,而且C語言陣列只能容納型別統一的基本資料型別,無法容納物件。好了,閒話少說,第19-24行的迴圈完成這樣一個功能:先建立一個新的SimpleThread類,然後將它放入vector中去,最後用thread.start()來啟動這個執行緒,為什麼要用start()方法來啟動執行緒呢?因為這是JAVA語言中所規定的,如果你不用的話,那這些執行緒將永遠得不到啟用,從而導致本示例程式根本無法執行。

  下面我們再來看一下process()方法,第30-40行的迴圈依次從vector陣列中選取SimpleThread執行緒,並檢查它是否處於啟用狀態(所謂啟用狀態是指此執行緒是否正在處理客戶端的請求),如果處於啟用狀態的話,那繼續查詢vector陣列的下一項,如果vector陣列中所有的執行緒都處於啟用狀態的話,那它會列印出一條資訊,提示使用者稍候再試。相反如果找到了一個睡眠執行緒的話,那第35-38行會對此進行處理,它先告訴客戶端是哪一個執行緒來處理這個請求,然後將客戶端的請求,即字串argument轉發給SimpleThread類的setArgument()方法進行處理,並呼叫SimpleThread類的setRunning()方法來喚醒當前執行緒,來對客戶端請求進行處理。

  可能你還對setRunning()方法是怎樣喚醒執行緒的有些不明白,那我們現在就進入最後一個類:SimpleThread類,它的原始碼如下:

//SimpleThread.java
1 class SimpleThread extends Thread
2 {
3 private boolean runningFlag;
4 private String argument;
5 public boolean isRunning()
6 {
7 return runningFlag;
8 }
9 public synchronized void setRunning(boolean flag)
10 {
11 runningFlag = flag;
12 if(flag)
13 this.notify();
14 }
15
16 public String getArgument()
17 {
18 return this.argument;
19 }
20 public void setArgument(String string)
21 {
22 argument = string;
23 }
24
25 public SimpleThread(int threadNumber)
26 {
27 runningFlag = false;
28 System.out.println("thread " + threadNumber + "started.");
29 }
30
31 public synchronized void run()
32 {
33 try{
34 while(true)
35 {
36 if(!runningFlag)
37 {
38 this.wait();
39 }
40 else
41 {
42 System.out.println("processing " + getArgument() + "... done.");
43 sleep(5000);
44 System.out.println("Thread is sleeping...");
45 setRunning(false);
46 }
47 }
48 } catch(InterruptedException e){
49 System.out.println("Interrupt");
50 }
51 }//end of run()
52 }//end of class SimpleThread

  如果你對JAVA的執行緒程式設計有些不太明白的話,那我先在這裡簡單地講解一下,JAVA有一個名為Thread的類,如果你要建立一個執行緒,則必須要從Thread類中繼承,並且還要實現Thread類的run()介面,要啟用一個執行緒,必須呼叫它的start()方法,start()方法會自動呼叫run()介面,因此使用者必須在run()介面中寫入自己的應用處理邏輯。那麼我們怎麼來控制執行緒的睡眠與喚醒呢?其實很簡單,JAVA語言為所有的物件都內建了wait()和notify()方法,當一個執行緒呼叫wait()方法時,則執行緒進入睡眠狀態,就像停在了當前程式碼上了,也不會繼續執行它以下的程式碼了,當呼叫notify()方法時,則會從呼叫wait()方法的那行程式碼繼續執行以下的程式碼,這個過程有點像編譯器中的斷點除錯的概念。以本程式為例,第38行呼叫了wait()方法,則這個執行緒就像凝固了一樣停在了38行上了,如果我們在第13行進行一個notify()呼叫的話,那執行緒會從第38行上喚醒,繼續從第39行開始執行以下的程式碼了。

  透過以上的講述,我們現在就不難理解SimpleThread類了,第9-14行透過設定一個標誌runningFlag啟用當前執行緒,第25-29行是SimpleThread類的建構函式,它用來告訴客戶端啟動的是第幾號程式。第31-50行則是我實現的run()介面,它實際上是一個無限迴圈,在迴圈中首先判斷一下標誌runningFlag,如果沒有runningFlag為false的話,那執行緒處理睡眠狀態,否則第42-45行會進行真正的處理:先列印使用者鍵入的字串,然後睡眠5秒鐘,為什麼要睡眠5秒鐘呢?如果你不加上這句程式碼的話,由於計算機處理速度遠遠超過你的鍵盤輸入速度,因此你看到的總是第1號執行緒來處理你的請求,從而達不到演示效果。最後第45行呼叫setRunning()方法又將執行緒置於睡眠狀態,等待新請求的到來。

  最後還有一點要注意的是,如果你在一個方法中呼叫了wait()和notify()函式,那你一定要將此方法置為同步的,即synchronized,否則在編譯時會報錯,並得到一個莫名其妙的訊息:“current thread not owner”(當前執行緒不是擁有者)。

  至此為止,我們完整地實現了一個執行緒池,當然,這個執行緒池只是簡單地將客戶端輸入的字串列印到了螢幕上,而沒有做任何處理,對於一個真正的企業級運用,本例還是遠遠不夠的,例如錯誤處理、執行緒的動態調整、效能最佳化、臨界區的處理、客戶端報文的定義等等都是值得考慮的問題,但本文的目的僅僅只是讓你瞭解執行緒池的概念以及它的簡單實現,如果你想成為這方面的高手,本文是遠遠不夠的,你應該參考一些更多的資料來深入地瞭解它。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/7199667/viewspace-916177/,如需轉載,請註明出處,否則將追究法律責任。

相關文章