三. ServerSocket 用法詳解(一)

 本文爲轉載文章,不用於商業活動,僅僅爲學習,如果大家對文章感興趣,請往原網址查閱更加具體的信息:http://blog.csdn.net/lin49940/article/details/4398364

 本篇文章觀點和例子來自 《Java網絡編程精解》, 作者爲孫衛琴, 出版社爲電子工業出版社。

      在客戶/服務器通信模式中, 服務器端需要創建監聽端口的 ServerSocket, ServerSocket 負責接收客戶連接請求. 本章首先介紹 ServerSocket 類的各個構造方法, 以及成員的用法, 接着介紹服務器如何用多線程來處理與多個客戶的通信任務.

      本章提供線程池的一種實現方法. 線程池包括一個工作隊列和若干工作線程. 服務器程序向工作隊列中加入與客戶通信的任務, 工作線程不斷從工作隊列中取出任務並執行它. 本章還介紹了 java.util.concurrent 包中的線程池類的用法, 在服務器程序中可以直接使用他們.

 

 一. 構造 ServerSocket

 

      ServerSocket 的構造方法有以下幾種重載形式:

  • ServerSocket() throws IOException
  • ServerSocket(int port) throws IOException
  • ServerSocket(int port, int backlog) throws IOException
  • ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

      在以上構造方法中, 參數 port 指定服務器要綁定的端口( 服務器要監聽的端口), 參數 backlog 指定客戶連接請求隊列的長度, 參數 bindAddr 指定服務器要綁定的IP 地址.

 

1.1 綁定端口

 

      除了第一個不帶參數的構造方法以外, 其他構造方法都會使服務器與特定端口綁定, 該端口有參數 port 指定. 例如, 以下代碼創建了一個與 80 端口綁定的服務器:

       ServerSocket serverSocket = new ServerSocket(80);                                  

      如果運行時無法綁定到 80 端口, 以上代碼會拋出 IOException, 更確切地說, 是拋出 BindException, 它是 IOException 的子類. BindException 一般是由以下原因造成的:

  • 端口已經被其他服務器進程佔用;
  • 在某些操作系統中, 如果沒有以超級用戶的身份來運行服務器程序, 那麼操作系統不允許服務器綁定到 1-1023 之間的端口.

      如果把參數 port 設爲 0, 表示由操作系統來爲服務器分配一個任意可用的端口. 有操作系統分配的端口也稱爲匿名端口. 對於多數服務器, 會使用明確的端口, 而不會使用匿名端口, 因爲客戶程序需要事先知道服務器的端口, 才能方便地訪問服務器. 在某些場合, 匿名端口有着特殊的用途, 本章 四 會對此作介紹.

 

1.2 設定客戶連接請求隊列的長度

 

      當服務器進程運行時, 可能會同時監聽到多個客戶的連接請求. 例如, 每當一個客戶進程執行以下代碼:

       Socket socket = new Socket("www.javathinker.org", 80);                                             

 

      就意味着在遠程 www.javathinker.org 主機的 80 端口上, 監聽到了一個客戶的連接請求. 管理客戶連接請求的任務是由操作系統來完成的. 操作系統把這些連接請求存儲在一個先進先出的隊列中. 許多操作系統限定了隊列的最大長度, 一般爲 50 . 當隊列中的連接請求達到了隊列的最大容量時, 服務器進程所在的主機會拒絕新的連接請求. 只有當服務器進程通過 ServerSocket 的 accept() 方法從隊列中取出連接請求, 使隊列騰出空位時, 隊列才能繼續加入新的連接請求.

 

      對於客戶進程, 如果它發出的連接請求被加入到服務器的請求連接隊列中, 就意味着客戶與服務器的連接建立成功, 客戶進程從 Socket 構造方法中正常返回. 如果客戶進程發出的連接請求被服務器拒絕, Socket 構造方法就會拋出 ConnectionException.

 

Tips: 創建綁定端口的服務器進程後, 當客戶進程的 Socket構造方法返回成功, 表示客戶進程的連接請求被加入到服務器進程的請求連接隊列中. 雖然客戶端成功返回 Socket對象, 但是還沒跟服務器進程形成一條通信線路. 必須在服務器進程通過 ServerSocket 的 accept() 方法從請求連接隊列中取出連接請求, 並返回一個Socket 對象後, 服務器進程這個Socket 對象才與客戶端的 Socket 對象形成一條通信線路.

 

      ServerSocket 構造方法的 backlog 參數用來顯式設置連接請求隊列的長度, 它將覆蓋操作系統限定的隊列的最大長度. 值得注意的是, 在以下幾種情況中, 仍然會採用操作系統限定的隊列的最大長度:

  • backlog 參數的值大於操作系統限定的隊列的最大長度;
  • backlog 參數的值小於或等於0;
  • 在ServerSocket 構造方法中沒有設置 backlog 參數.


      以下的 Client.java 和 Server.java 用來演示服務器的連接請求隊列的特性.

 

      Client.java 略..... , Server.java 略......

 

      Client 試圖與 Server 進行 100 次連接. 在 Server 類中, 把連接請求隊列的長度設爲 3. 這意味着當隊列中有了 3 個連接請求時, 如果Client 再請求連接, 就會被 Server 拒絕.  下面按照以下步驟運行 Server 和 Client 程序.

 

      ⑴ 在Server 中只創建一個 ServerSocket 對象, 在構造方法中指定監聽的端口爲8000 和 連接請求隊列的長度爲 3 . 構造 Server 對象後, Server 程序睡眠 10 分鐘, 並且在 Server 中不執行 serverSocket.accept() 方法. 這意味着隊列中的連接請求永遠不會被取出. 運行Server 程序和 Client 程序後, Client程序的打印結果如下:

 第 1 次連接成功
 第 2 次連接成功
 第 3 次連接成功
 Exception in thread "main" java.net.ConnectException: Connection refused: connect
 ................

      從以上打印的結果可以看出, Client 與 Server 在成功地建立了3 個連接後, 就無法再創建其餘的連接了, 因爲服務器的隊已經滿了.

 

      ⑵ 在Server中構造一個跟 ⑴ 相同的 ServerSocket對象, Server程序不睡眠, 在一個 while 循環中不斷執行 serverSocket.accept()方法, 該方法從隊列中取出連接請求, 使得隊列能及時騰出空位, 以容納新的連接請求. Client 程序的打印結果如下:

  第 1 次連接成功
  第 2 次連接成功
  第 3 次連接成功
  ...........
  第 100 次連接成功

      從以上打印結果可以看出, 此時 Client 能順利與 Server 建立 100 次連接.(每次while的循環要夠快才行, 如果太慢, 從隊列取連接請求的速度比放連接請求的速度慢的話, 不一定都能成功連接) 

 

1.3 設定綁定的IP 地址

 

      如果主機只有一個IP 地址, 那麼默認情況下, 服務器程序就與該IP 地址綁定. ServerSocket 的第 4 個構造方法 ServerSocket(int port, int backlog, InetAddress bingAddr) 有一個 bindAddr 參數, 它顯式指定服務器要綁定的IP 地址, 該構造方法適用於具有多個IP 地址的主機. 假定一個主機有兩個網卡, 一個網卡用於連接到 Internet, IP爲 222.67.5.94, 還有一個網卡用於連接到本地局域網, IP 地址爲 192.168.3.4. 如果服務器僅僅被本地局域網中的客戶訪問, 那麼可以按如下方式創建 ServerSocket:

       ServerSocket serverSocket = new ServerSocket(8000, 10, InetAddress.getByName("192.168.3.4"));

 

1.4 默認構造方法的作用

 

      ServerSocket 有一個不帶參數的默認構造方法. 通過該方法創建的 ServerSocket 不與任何端口綁定, 接下來還需要通過 bind() 方法與特定端口綁定.

      這個默認構造方法的用途是, 允許服務器在綁定到特定端口之前, 先設置ServerSocket 的一些選項. 因爲一旦服務器與特定端口綁定, 有些選項就不能再改變了.

 

      在以下代碼中, 先把 ServerSocket 的 SO_REUSEADDR 選項設爲 true, 然後再把它與 8000 端口綁定:

  ServerSocket serverSocket = new ServerSocket();
  serverSocket.setReuseAddress(true);              //設置 ServerSocket 的選項
  serverSocket.bind(new InetSocketAddress(8000));  //與8000端口綁定

      如果把以上程序代碼改爲:

  ServerSocket serverSocket = new ServerSocket(8000);
  serverSocket.setReuseAddress(true);              //設置 ServerSocket 的選項

      那麼 serverSocket.setReuseAddress(true) 方法就不起任何作用了, 因爲 SO_REUSEADDR 選項必須在服務器綁定端口之前設置纔有效.

 

二. 接收和關閉與客戶的連接

 

      ServerSocket 的 accept() 方法從連接請求隊列中取出一個客戶的連接請求, 然後創建與客戶連接的 Socket 對象, 並將它返回. 如果隊列中沒有連接請求, accept() 方法就會一直等待, 直到接收到了連接請求才返回.

 

      接下來, 服務器從 Socket 對象中獲得輸入流和輸出流, 就能與客戶交換數據. 當服務器正在進行發送數據的操作時, 如果客戶端斷開了連接, 那麼服務器端會拋出一個IOException 的子類 SocketException 異常:

        java.net.SocketException: Connection reset by peer                                           

      這只是服務器與單個客戶通信中出現的異常, 這種異常應該被捕獲, 使得服務器能繼續與其他客戶通信.

 

      以下程序顯示了單線程服務器採用的通信流程:

 

 public void service(){
  while(true){
   Socket socket = null;
   try{
       socket = serverSocket.accept();    //從連接請求隊列中取出一個連接
    System.out.println("New connection accepted " 
      + socket.getInetAddress()  + " : " + socket.getPort());
    //接收和發送數據
          ............
   }catch(IOException e)
     //這只是與單個客戶通信時遇到的異常, 可能是由於客戶端過早斷開連接引起
     //這種異常不應該中斷整個while循環
    e.printStackTrace();
   }finally{
    try{
     if(socket != null) socket.close();  //與一個客戶通信結束後, 要關閉Socket
    }catch(IOException e){
     e.printStackTrace();}
   }
  }
 }

 

      與單個客戶通信的代碼放在一個try 代碼塊中, 如果遇到異常, 該異常被catch 代碼塊捕獲. try 代碼塊後面還有一個finally 代碼塊, 它保證不管與客戶通信正常結果還是異常結束, 最後都會關閉Socket, 斷開與這個客戶的連接.

 

三. 關閉ServerSocket

 

     ServerSocket 的 close() 方法使服務器釋放佔用的端口, 並且斷開與所有客戶的連接. 當一個服務器程序運行結束時, 即使沒有執行 ServerSocket 的 close() 方法, 操作系統也會釋放這個服務器佔用的端口. 因此, 服務器程序不一定要在結束之前執行 ServerSocket 的 close() 方法.

 

      在某些情況下, 如果希望及時釋放服務器的端口, 以便讓其他程序能佔用該端口, 則可以顯式調用 ServerSocket 的 close() 方法. 例如, 以下代碼用於掃描 1-65535 之間的端口號. 如果 ServerSocket 成功創建, 意味這該端口未被其他服務器進程綁定, 否則說明該端口已經被其他進程佔用:

  for(int port = 1; port <= 65335; port ++){
   try{
    ServerSocket serverSocket = new ServerSocket(port);
    serverSocket.close();   //及時關閉ServerSocket
   }catch(IOException e){
    System.out.println("端口" + port + " 已經被其他服務器進程佔用");
   }   
  }

 

     以上程序代碼創建了一個 ServerSocket 對象後, 就馬上關閉它, 以便及時釋放它佔用的端口, 從而避免程序臨時佔用系統的大多數端口.

 

     ServerSocket 的 isClosed() 方法判斷 ServerSocket 是否關閉, 只有執行了 ServerSocket 的 close()方法, isClosed() 方法才返回 true; 否則, 即使 ServerSocket 還沒有和特定端口綁定, isClosed() 也會返回 false.

 

      ServerSocket 的 isBound() 方法判斷 ServerSocket 是否已經與一個端口綁定, 只要 ServerSocket 已經與一個端口綁定, 即使它已經被關閉, isBound() 方法也會返回 true.

 

      如果需要確定一個 ServerSocket 已經與特定端口綁定, 並且還沒有被關閉, 則可以採用以下方式:

        boolean isOpen = serverSocket.isBound() && !serverSocket.isClosed();                             

 

四. 獲取ServerSocket 的信息

 

     ServerSocket 的以下兩個 get 方法可以分別獲得服務器綁定的 IP 地址, 以及綁定的端口:

  • public InetAddress getInetAddress();
  • public int getLocalPort()

     前面已經講到, 在構造 ServerSocket 時, 如果把端口設爲 0 , 那麼將有操作系統爲服務器分配一個端口(稱爲匿名端口), 程序只要調用 getLocalPort() 方法就能獲知這個端口號. 如下面的 RandomPort 創建了一個 ServerSocket, 它使用的就是匿名端口.

 

       RandomPort.java 略...........

 

      多數服務器會監聽固定的端口, 這樣才便於客戶程序訪問服務器. 匿名端口一般設用於服務器與客戶之間的臨時通信, 通信結束, 就斷開連接, 並且 ServerSocket 佔用的臨時端口也被釋放.

 

      FTP(文件傳輸協議) 就使用了匿名端口.  FTP協議用於在本地文件系統與遠程文件系統之間傳送文件.

 

      FTP 使用兩個並行的TCP 連接: 一個是控制連接, 一個是數據連接. 控制連接用於在客戶和服務器之間發送控制信息, 如用戶名和口令、改變遠程目錄的命令或上傳和下載文件的命令. 數據連接用於傳送而文件. TCP 服務器在 21 端口上監聽控制連接, 如果有客戶要求上傳或下載文件, 就另外建立一個數據連接, 通過它來傳送文件. 數據連接的建立有兩種方式.

 

      ⑴ TCP 服務器在 20 端口上監聽數據連接, TCP 客戶主動請求建立與該端口的連接.

 

      ⑵ 首先由 TCP 客戶創建一個監聽匿名端口的 ServerSocket, 再把這個 ServerSocket 監聽的端口號發送給 TCP 服務器, 然後由TCP 服務器主動請求建立與客戶端的連接.

 

      以上第二種方式就使用了匿名端口, 並且是在客戶端使用的, 用於和服務器建立臨時的數據連接. 在實際應用中, 在服務器端也可以使用匿名端口.

 

五. ServerSocket 選項

 

      ServerSocket 有以下 3 個選項.

  • SO_TIMEOUT: 表示等待客戶連接的超時時間.
  • SO_REUSEADDR: 表示是否允許重用服務器所綁定的地址.
  • SO_RCVBUF: 表示接收數據的緩衝區的大小.

5.1 SO_TIMEOUT 選項

  • 設置該選項: public void setSoTimeout(int timeout) throws SocketException
  • 讀取該選項: public int getSoTimeout() throws SocketException

      SO_TIMEOUT 表示 ServerSocket 的 accept() 方法等待客戶連接的超時時間, 以毫秒爲單位. 如果SO_TIMEOUT 的值爲 0 , 表示永遠不會超時, 這是 SO_TIMEOUT 的默認值.

 

      當服務器執行 ServerSocket 的 accept() 方法是, 如果連接請求隊列爲空, 服務器就會一直等待, 直到接收到了客戶連接才從 accept() 方法返回. 如果設定了超時時間, 那麼當服務器等待的時間查歐哦了超時時間, 就會拋出 SocketTimeoutException, 它是 InterruptedException 的子類.

        java.net.SocketTimeoutException: Accept timed out                                                             

 

Tips: 服務器執行 serverSocket.accept() 方法時, 等待客戶連接的過程也稱爲阻塞. 本書第 4 章的第一節詳細介紹了阻塞的概念.

 

5.2 SO_REUSEADDR 選項

  • 設置該選項: public void setResuseAddress(boolean on) throws SocketException
  • 讀取該選項: public boolean getResuseAddress() throws SocketException

      這個選項與Socket 的選項相同, 用於決定如果網絡上仍然有數據向舊的 ServerSocket 傳輸數據, 是否允許新的 ServerSocket 綁定到與舊的 ServerSocket 同樣的端口上. SO_REUSEADDR 選項的默認值與操作系統有關, 在某些操作系統中, 允許重用端口, 而在某些操作系統中不允許重用端口.

  

      當 ServerSocket 關閉時, 如果網絡上還有發送到這個 ServerSocket 的數據, 這個ServerSocket 不會立即釋放本地端口, 而是會等待一段時間, 確保接收到了網絡上發送過來的延遲數據, 然後再釋放端口.

 

      許多服務器程序都使用固定的端口. 當服務器程序關閉後, 有可能它的端口還會被佔用一段時間, 如果此時立刻在同一個主機上重啓服務器程序, 由於端口已經被佔用, 使得服務器程序無法綁定到該端口, 服務器啓動失敗, 並拋出 BindException:

     java.net.BindExcetpion: Address already in use: JVM_Bind                                               

 

      爲了確保一個進程關閉了 ServerSocket 後, 即使操作系統還沒釋放端口, 同一個主機上的其他進程還可以立即重用該端口, 可以調用 ServerSocket 的 setResuseAddress(true) 方法:

        if(!serverSocket.getReuseAddress()) serverSocket.setReuseAddress(true);                                  

 

      值得注意的是, serverSocket.setReuseAddress(true) 方法必須在 ServerSocket 還沒有綁定到一個本地端口之前調用, 否則執行 serverSocket.setReuseAddress(true) 方法無效. 此外, 兩個共用同一個端口的進程必須都調用 serverSocket.setResuseAddress(true) 方法, 才能使得一個進程關閉 ServerSocket 後, 另一個進程的 ServerSocket 還能夠立刻重用相同的端口.

 

5.3 SO_RCVBUF 選項

  • 設置該選項: public void setReceiveBufferSize(int size) throws SocketException
  • 讀取該選項: public int getReceiveBufferSize() throws SocketException

      SO_RCVBUF 表示服務器端的用於接收數據的緩衝區的大小, 以字節爲單位. 一般說來, 傳輸大的連續的數據塊(基於HTTP 或 FTP 協議的數據傳輸) 可以使用較大的緩衝區, 這可以減少傳輸數據的次數, 從而提高傳輸數據的效率. 而對於交互頻繁且單次傳送數量比較小的通信(Telnet 和 網絡遊戲), 則應該採用小的緩衝區, 確保能及時把小批量的數據發送給對方.

 

      SO_RCVBUF 的默認值與操作系統有關. 例如, 在Windows 2000 中運行以下代碼時, 顯示 SO_RCVBUF 的默認值爲 8192.

 

      無論在 ServerSocket綁定到特定端口之前或之後, 調用 setReceiveBufferSize() 方法都有效. 例外情況下是如果要設置大於 64 KB 的緩衝區, 則必須在 ServerSocket 綁定到特定端口之前進行設置纔有效.

 

      執行 serverSocket.setReceiveBufferSize() 方法, 相當於對所有由 serverSocket.accept() 方法返回的 Socket 設置接收數據的緩衝區的大小.

 

5.4 設定連接時間、延遲和帶寬的相對重要性

  • public void setPerformancePreferences(int connectionTime, int latency, int bandwidth)

     該方法的作用與 Socket 的 setPerformancePreferences 方法的作用相同, 用於設定連接時間、延遲和帶寬的相對重要性, 參見 第二章的 5.10.

 

六. 創建多線程的服務器

 

      在第一章的 EchoServer中, 其 service()方法負責接收客戶連接, 以及與客戶通信. service() 方法的處理流程, EchoServer 接收到一個客戶連接, 就與客戶進行通信, 通信完畢後斷開連接, 然後在接收下一個客戶. 假如同時有多個客戶請求連接, 這些客戶就必須排隊等候EchoServer 的響應. EchoServer 無法同時與多個客戶通信.

 

      許多實際應用要求服務器具有同時爲多個客戶提供服務的能力. HTTP 服務器就是最明顯的例子. 任何時刻, HTTP 服務器都可能接收到大量的客戶請求, 每個客戶都希望能快速得到HTTP 服務器的響應. 如果長時間讓客戶等待, 會使網站失去信譽, 從而降低訪問量.

 

      可以用併發性能來衡量一個服務器同時響應多個客戶的能力. 一個具有好的併發性能的服務器, 必須符合兩個條件:

  • 能同時接收並處理多個客戶連接;
  • 對於每個客戶, 都會迅速給予響應.

     服務器同時處理的客戶連接數目越多, 並且對每個客戶作出響應的速度越快, 就表明併發性能越高.

 

     用多個線程來同時爲多個客戶提供服務, 這是提高服務器的併發性能的最常用的手段. 本結將按照 3 種方式來重新實現 EchoServer, 它們都使用了多線程.

  • 爲每個客戶分配一個工作線程
  • 創建一個線程池, 由其中的工作線程來爲客戶服務.
  • 利用JDK 的 Java 類庫中現成的線程池, 有它的工作線程來爲客戶服務.

6.1 爲每個客戶分配一個線程

 

      服務器的主線程負責接收客戶的連接, 每次接收到一個客戶連接, 就會創建一個工作線程, 由它負責與客戶的通信. 以下是 EchoServer  的 service() 方法的代碼:

 public void service(){
  while(true){
   Socket socket = null;
   try{
    socket = serverSocket.accept();           //接收客戶連接
    Thread workThread = new Thread(new Handler(socket));         //創建一個工作進程
    workThread.start();        //啓動工作進程
      }catch(IOException e){
       e.printStackTrace();
      }
  }
 }

 

      以上工作線程 workThread 執行 Handler 的 run() 方法. Handler 類實現了 Runnable 接口, 它的 run() 方法負責與單個客戶通信, 與客戶通信結束後, 就會斷開連接, 執行 Handler 的 run() 方法的工作線程也會自然終止. 下面是 EchoServer 類及 Handler 類的源代碼.

 

EchoServer.java(爲每個任務分配一個線程)

package multithread1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoServer {
 
 private int port = 8000;
 private ServerSocket serverSocket;
 
 public EchoServer() throws IOException{
  serverSocket = new ServerSocket(8000);
  System.out.println("服務器啓動");
 }
 
 public void service(){
  while(true){
   Socket socket = null;
   try{
    socket = serverSocket.accept();           //接收客戶連接
    Thread workThread = new Thread(new Handler(socket));         //創建一個工作進程
    workThread.start();        //啓動工作進程
      }catch(IOException e){
       e.printStackTrace();
      }
  }
 }
 
 public static void main(String[] args) throws IOException {
  // TODO Auto-generated method stub
  new EchoServer().service();
 }
 
 class Handler implements Runnable{

  private Socket socket; 
  
  public Handler(Socket socket) {
   this.socket = socket;
  }
  
  private PrintWriter getWriter(Socket socket) throws IOException{
   OutputStream socketOut = socket.getOutputStream();
   return new PrintWriter(socketOut,true);
  }
  
  private BufferedReader getReader(Socket socket) throws IOException{
   InputStream socketIn = socket.getInputStream();
   return new BufferedReader(new InputStreamReader(socketIn));
  }

  public String echo(String msg){
   return "echo:" + msg;
  }

  public void run() {
   // TODO Auto-generated method stub
   try{
    System.out.println("New connection accepted " 
      + socket.getInetAddress() + ":" + socket.getPort());
    BufferedReader br = getReader(socket);
    PrintWriter pw = getWriter(socket);
    
    String msg = null;
    while((msg = br.readLine()) != null){   //接收和發送數據, 直到通信結束
     System.out.println(msg);
     pw.println(echo(msg));
     if(msg.equals("bye")){
      break;
     }
     
    }
   }catch(IOException e){
    e.printStackTrace();
   }finally{
    try{
     if(socket != null) socket.close();             //斷開連接
    }catch(IOException e){}
   }
  }
  
 }

} 

 

 6.2 創建線程池

   

      在 6.1 節介紹的實現方式中, 對每個客戶都分配一個新的工作進程. 當工作線程與客戶通信結束, 這個線程就被銷燬. 這種實現方式有以下不足之處.

  • 服務器創建和銷燬工作線程的開銷(包括所花費的時間和系統資源) 很大. 如果服務器需要與多個客戶通信, 並且與每個客戶的通信時間都很短, 那麼有可能服務器爲客戶創建新線程的開銷比實際與客戶通信的開銷還要大.
  • 除了創建和銷燬線程的開銷之外, 活動的線程也消耗系統資源.每個線程本書都會佔用一定的內存(每個線程需要大約 1MB 內存), 如果同時有大量客戶連接服務器, 就必須創建大量工作線程, 它們消耗了大量內存, 可能會導致系統的內存空間不足.
  • 如果線程數目固定, 並且每個線程都有很長的生命週期, 那麼線程切換也是相對固定的. 不同的操作系統有不同的切換週期, 一般在 20 毫秒左右. 這裏所說的線程切換是指在 Java 虛擬機, 以及底層操作系統的調度下, 線程之間轉讓 CPU 的使用權. 如果頻繁創建和銷燬進程, 那麼將導致頻繁地切換線程, 因爲一個線程被銷燬後, 必然要把 CPU 轉讓給另一個已經就緒的線程, 使該線程獲得運行的機會. 在這種情況下, 線程之間不再遵循系統的固定切換週期, 切換進程的開銷甚至比創建及銷燬線程的開銷還大.

      線程池爲線程生命週期開銷問題和系統資源不足問題提供瞭解決方案. 線程池中預先創建了一些工作線程, 它們不斷從工作隊列中取出任務, 然後執行該任務. 當工作線程執行完一個任務時, 就會繼續執行工作隊列中的下一個任務. 線程池具有以下的優點:

  • 減少了創建和銷燬線程的次數, 每個工作線程都可以一直被重用, 能執行多個任務.
  • 可以根據系統的承載能力, 方便地調整線程池中線程的數目, 防止因爲消耗過量系統資源而導致系統崩潰.

     下面的 ThreadPool 類提供了線程池的一種實現方案.

 

        ThreadPool.java

package multithread2;

import java.util.LinkedList;

public class ThreadPool extends ThreadGroup {
 private boolean isClosed = false;   //線程池是否關閉
 private LinkedList<Runnable> workQueue;  //工作隊列
    private static int threadPoolID;   //表示線程池ID
    private int threadID;      //表示工作線程ID ,因爲 WorkThread是內部類所以纔可以這樣
  

 public ThreadPool(int poolSize) {       //poolSize指定線程池中的工作線程數目
  super("ThreadPool-" + (threadPoolID ++));
  this.setDaemon(true);
  workQueue = new LinkedList<Runnable>();      //創建工作隊列
  for(int i=0; i<poolSize; i++){
   new WorkThread().start();                //創建並啓動工作進程
  }
 }
 public WorkThread getWorkThread(){
  return new WorkThread();
 }

 /**
  * 向工作隊列中加入一個新任務, 由工作進程去執行該任務
  */
 public synchronized void execute(Runnable task){
  if(isClosed){              //線程池被關閉則拋出IllegalStateException 異常
   throw new IllegalStateException();
  }
  if(task != null){
   workQueue.add(task);      //添加任務到工作隊列中
   notify();                 //呼醒正在getTask()方法中等待任務的某一個工作線程,哪一個是隨機的
  }
 }
 
 /**
  * @throws InterruptedException wait()中被interrupt()將產生這個異常
  * 從工作隊列中取出一個任務, 工作線程會調用此方法
  */
 protected synchronized Runnable getTask() throws InterruptedException{
  while(workQueue.size() == 0){
   if(isClosed) return null;
   wait();   //如果工作隊列中沒有任務, 就等待任務, 對應execute()方法中的notify和join()方法中的notifyAll()
  }
  return workQueue.removeFirst();
 }
 
 /** 關閉線程池 */
 public synchronized void close(){
  if(!isClosed){
   isClosed = true;
   workQueue.clear();    //清空工作隊列
   interrupt();                 //中斷所有的的工作線程, 該方法繼承自 ThreadGroup 類
  }
 }
 
 /** 等待工作線程把所有任務執行完 */
 public void join(){
  synchronized(this){
   isClosed = true;
   notifyAll();            //呼醒所有在getTask()方法中等待任務的工作線程
  }
  Thread[] threads = new Thread[this.activeCount()];

  //enumerate()方法繼承自 ThreadGroup 類, 獲得線程組中當前所有活着的工作進程 ,並把這些線程放到指定的Thread數組中
  int count = this.enumerate(threads);
  for(int i=0; i<count; i++){         //等待所有工作線程運行結束
   try{
    threads[i].join();          //等待工作進程運行結束
   }catch(InterruptedException ex){}
  }
 } 
  
    /** 內部類: 工作線程(注意, 在本類中, 內部類(不是static的)和外部類可以互相訪問對方的所有成員變量和方法, 即使是private的方法和成員變量, 
     *                  本類內部訪問限定符對內部類和外部類都沒有影響, 就好像是在所有成員變量和方法視爲一個同等的存在一樣處理.
     *                  但是如果你是在其他類中就訪問本類中的外部類和內部類的成員變量和方法, 那就會受到訪問限定符的限制的.
     *                  最後還要注意, 不要在本類中的main() 中測試, 理由不用說了吧) 
*/
 private class WorkThread extends Thread {
  public WorkThread() {
   // 加入到當前 ThreadPool 線程當中
   super(ThreadPool.this, "WorkThread-" + (threadID++)); 
  }

  public void run() {
   while(!isInterrupted()){       //isInterrupted() 方法繼承自 Thread 類, 判斷線程是否被中斷
    Runnable task = null;
    try{
     task = getTask();   //取出任務
    }catch(InterruptedException e){}
    
    //如果getTask() 返回null, 表示線程池已經被關閉了, 結束此線程
    if(task == null) return;

    try{
     task.run();
    }catch(Throwable t){ //捕捉線程run()運行中產生所有的錯誤和異常,爲了防止進程被結束
     t.printStackTrace();
    }

   }//while
  }//run
 }//WorkThread

}

 

 

 

由於篇幅問題, 接下的內容在 三. ServerSocket 用法詳解(二) 中繼續

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章