Java代碼構建一個線程池

在現代的操做系統中,有一個很重要的概念――線程,幾乎全部目前流行的操做系統都支持線程,線程來源於操做系統中進程的概念,進程有本身的虛擬地址空間以及正文段、數據段及堆棧,並且各自佔有不一樣的系統資源(例如文件、環境變量等等)。與此不一樣,線程不能單獨存在,它依附於進程,只能由進程派生。若是一個進程派生出了兩個線程,那這兩個線程共享此進程的全局變量和代碼段,但每一個線程各擁有各自的堆棧,所以它們擁有各自的局部變量,線程在UNIX系統中還被進一步分爲用戶級線程(由進程自已來管理)和系統級線程(由操做系統的調度程序來管理)。   既然有了進程,爲何還要提出線程的概念呢?由於與建立一個新的進程相比,建立一個線程將會耗費小得多的系統資源,對於一些小型的應用,可能感受不到這點,但對於那些併發進程數特別多的應用,使用線程會比使用進程得到更好的性能,從而下降操做系統的負擔。另外,線程共享建立它的進程的全局變量,所以線程間的通信編程會更將簡單,徹底能夠拋棄傳統的進程間通信的IPC編程,而採用共享全局變量來進行線程間通信。   有了上面這個概念,咱們下面就進入正題,來看一下線程池到底是怎麼一回事?其實線程池的原理很簡單,相似於操做系統中的緩衝區的概念,它的流程以下:先啓動若干數量的線程,並讓這些線程都處於睡眠狀態,當客戶端有一個新請求時,就會喚醒線程池中的某一個睡眠線程,讓它來處理客戶端的這個請求,當處理完這個請求後,線程又處於睡眠狀態。可能你也許會問:爲何要搞得這麼麻煩,若是每當客戶端有新的請求時,我就建立一個新的線程不就完了?這也許是個不錯的方法,由於它能使得你編寫代碼相對容易一些,但你卻忽略了一個重要的問題――性能!就拿我所在的單位來講,個人單位是一個省級數據大集中的銀行網絡中心,高峯期每秒的客戶端請求併發數超過100,若是爲每一個客戶端請求建立一個新線程的話,那耗費的CPU時間和內存將是驚人的,若是採用一個擁有200個線程的線程池,那將會節約大量的的系統資源,使得更多的CPU時間和內存用來處理實際的商業應用,而不是頻繁的線程建立與銷燬。   既然一切都明白了,那咱們就開始着手實現一個真正的線程池吧,線程編程能夠有多種語言來實現,例如C、C++、java等等,但不一樣的操做系統提供不一樣的線程API接口,爲了讓你能更明白線程池的原理而避免陷入煩瑣的API調用之中,我採用了JAVA語言來實現它,因爲JAVA語言是一種跨平臺的語言,所以你沒必要爲使用不一樣的操做系統而沒法編譯運行本程序而苦惱,只要你安裝了JDK1.2以上的版本,都能正確地編譯運行本程序。另外JAVA語言自己就內置了線程對象,並且JAVA語言是徹底面像對象的,所以可以讓你更清晰地瞭解線程池的原理,若是你注意看一下本文的標題,你會發現整個示例程序的代碼只有大約100行。   本示例程序由三個類構成,第一個是TestThreadPool類,它是一個測試程序,用來模擬客戶端的請求,當你運行它時,系統首先會顯示線程池的初始化信息,而後提示你從鍵盤上輸入字符串,並按下回車鍵,這時你會發現屏幕上顯示信息,告訴你某個線程正在處理你的請求,若是你快速地輸入一行行字符串,那麼你會發現線程池中不斷有線程被喚醒,來處理你的請求,在本例中,我建立了一個擁有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」(當前線程不是擁有者)。   至此爲止,咱們完整地實現了一個線程池,固然,這個線程池只是簡單地將客戶端輸入的字符串打印到了屏幕上,而沒有作任何處理,對於一個真正的企業級運用,本例仍是遠遠不夠的,例如錯誤處理、線程的動態調整、性能優化、臨界區的處理、客戶端報文的定義等等都是值得考慮的問題,但本文的目的僅僅只是讓你瞭解線程池的概念以及它的簡單實現,若是你想成爲這方面的高手,本文是遠遠不夠的,你應該參考一些更多的資料來深刻地瞭解它。
相關文章
相關標籤/搜索