1. 程式人生 > >【Java.NIO】Java NIO中的阻塞 V.S. 非阻塞

【Java.NIO】Java NIO中的阻塞 V.S. 非阻塞

從JDK1.4版本開始,引入了非阻塞的通訊機制

伺服器程式接受客戶連線,客戶程式建立與伺服器的連線,以及伺服器程式和客戶端程式收發資料的操作都可以按非阻塞的方式進行。伺服器程式只需要建立一個執行緒,就能完成同時與多個客戶通訊的任務。

執行緒阻塞

執行緒在執行中會因為某些原因而阻塞,所有處於阻塞狀態的執行緒的共同特徵是:

放棄CPU,暫停執行,只有等到導致阻塞的原因消除,才能恢復執行;或者被其他執行緒中斷,該執行緒會退出阻塞狀態,並且丟擲InterruptedException。

執行緒阻塞的原因

導致執行緒阻塞的原因主要有以下幾方面:

  • 執行緒執行了Thread.sleep(int n)方法,即執行緒放棄CPU,睡眠n毫秒,然後恢復執行
  • 執行緒要執行一段同步程式碼,由於無法獲得相關的同步鎖,進入阻塞狀態,等待獲得了同步鎖,才能恢復執行
  • 執行緒執行一個物件的wait()方法,進入阻塞狀態,只有等到其他執行緒執行了該物件的notify()或notifyAll()方法,才可能將其喚醒
  • 執行緒執行I/O操作或進行遠端通訊時,會因為等待相關的資源而進入阻塞狀態

在進行遠端通訊時,在客戶程式中,執行緒在以下情況可能進入阻塞狀態:

  • 請求與伺服器建立連線時,即當執行緒執行Socket的帶引數的構造方法,或執行Socket的connect()方法時,會進入阻塞狀態,直到連線成功,此執行緒才從Socket的構造方法或connect()方法返回
  • 執行緒從Socket的輸入流讀入資料時,如果沒有足夠的資料,就會進入阻塞狀態,直到讀到了足夠的資料,或者到達了輸入流的末尾,或者出現了異常,才從輸入流的read()方法返回或異常中斷。輸入流中有多少資料才算足夠,這要看執行緒執行的read()方法的型別:
    • int read():只要輸入流中要一個位元組,就算足夠
    • int read(byte[] buff): 只要輸入流中的位元組數與引數buff陣列的長度相同,就算足夠
    • String readLine():只要輸入流中有一行字串,就算足夠
  • 執行緒向Socket輸出流中寫一批資料時,可能會進入阻塞狀態,等到輸出了所有的資料,或者出現異常,才從輸出流的write()方法返回或異常中斷
  • 呼叫Socket的setSoLinger()方法設定了關閉Socket的延遲事件,那麼當執行緒執行Socket的close()方法時,會進入阻塞狀態,直到底層Socket傳送完所有剩餘資料,或者超過了方法設定的延遲時間,才從close()方法返回

在伺服器程式中,執行緒在一下情況下可能會進入阻塞狀態:

  • 執行緒執行ServerSocket的accept()方法,等待客戶的連線,直到收到了客戶連線,才從accept()方法返回
  • 執行緒從Socket的輸入流讀入資料,如果輸入流沒有足夠的資料,就會進入阻塞狀態
  • 執行緒向Socket的輸出流寫一批資料時,可能會進入阻塞狀態,等到輸出了所有的資料,或者出現異常,才從輸出流的write()方法返回或異常中斷

這種可能出現阻塞的輸入和輸出操作被稱為阻塞I/O。與此對照,如果執行輸入和輸出操作時,不會發生阻塞,則稱為非阻塞I/O。

伺服器程式用多執行緒處理阻塞通訊的侷限

Java虛擬機器會為每個執行緒分配獨立的堆疊空間,工作執行緒數目越多,系統開銷就越大,而且增加了Java虛擬機器排程執行緒的負擔,增減了執行緒之間同步的複雜性,提高了執行緒死鎖的可能性

工作執行緒的時間大多浪費在阻塞I/O操作上,Java虛擬機器需要頻繁的轉讓CPU的使用權,使進入阻塞狀態的執行緒放棄CPU,在把CPU分配給處於可執行狀態的執行緒。

保持適當的工作執行緒,會提高伺服器的併發效能,但是當工作執行緒的數目達到某個極限,超出了系統的負荷,反而會降低併發效能,使得多數客戶無法快速得到伺服器的響應。

非阻塞通訊

所謂非阻塞,就是指當執行緒執行這些方法時,如果操作還沒有就緒,就立即返回,而不會一直等到操作就緒。

例如:當執行緒接受客戶連線時,如果沒有客戶連線,就立即返回;或者當執行緒從輸入流中讀資料時,如果輸入流中還沒有資料,就立即返回;或者如果輸入流中還沒有足夠的資料,那麼就讀取現有的資料,然後返回。

while (一直等待,直到有接受連線就緒事件,或讀就緒事件,或寫就緒事件發生時){  // 阻塞
    if (有客戶連線)
        接收客戶的連線;                                             // 非阻塞
    if (某個Socket的輸入流中有可讀資料)
        從輸入流中讀資料;                                           // 非阻塞
    if (某個Socket的輸出流可以寫資料)
        向輸出流寫資料;                                             // 非阻塞
}


注意:上面while迴圈條件中的操作還是按照阻塞方式進行的,如果未發生任何事件,就會進入阻塞狀態,直到接受連線就緒,讀就緒事件或寫就緒事件中至少有一個事件發生時,才會執行while迴圈體中的操作。在while迴圈中,一般會包含在特定條件下退出迴圈的操作。