標籤:輪詢 驅動 tty 複用 線程 監聽 add end bsp
在進入主題之前先看個 Java 網路編程的一個簡單例子:代碼很簡單,用戶端和服務端進行通訊,對於用戶端的每次輸入,服務端回複 get。注意,服務端可以同時允許多個用戶端串連。
服務端端代碼:
// 建立服務端 socketServerSocket serverSocket = new ServerSocket(20000);client = serverSocket.accept();// 用戶端串連成功,輸出提示System.out.println("用戶端串連成功");// 啟動一個新的線程處理用戶端請求new Thread(new ServerThread(client)).start();// 子線程中處理用戶端的輸入class ServerThread implements Runnable { ..... @Override public void run() { boolean flag = true; while (flag) { // 讀取用戶端發送來的資料 String str = buf.readLine(); // 回複給用戶端 get 表示收到資料 out.println("get"); } }}
用戶端代碼 :
Socket client = new Socket("127.0.0.1", 20000);boolean flag = true;while (flag) { // 讀取使用者從鍵盤的輸入 String str = input.readLine(); // 把使用者的輸入發送給服務端 out.println(str); // 接受到服務端回傳的 get 字串 String echo = buf.readLine(); System.out.println(echo); }}
考慮到完整的 Java 範例程式碼太過龐大影響閱讀,所以這裡不完整貼出,如果需要在 github 直接下載,這裡是。
可以看到,server 為了能夠同時處理多個 client 的請求,需要為每個 client 開啟一個 thread,這種 one-thread-per-client 的模式對於 server 而言壓力是很大的。假設有 1k 個 client,對應的 server 應該啟動 1k 個 thread,那麼 server 所耗費的記憶體,以及 thread 切換時候佔用的時間等等都是致命傷。即使使用線程池的技術來限制線程個數,這種 blocking-IO 的模型還是沒辦法支撐大量串連。
每個 client 都需要一個 thread 來請求處理。
NIO
上面這種 one-thread-per-client 的模式無法支撐大量串連的主要原因在於 readLine 會 阻塞 IO,即在 readLine 沒能夠讀取到資料的時候,會一直阻塞線程,使得線程無法繼續執行,那麼 server 為了可以同時處理多個 client,只能同時開啟多個線程。
所以,Java 1.4 之後引入了一套 NIO 介面。NIO 中最主要的一個功能就是可以進行非阻塞 IO 操作:如果沒能夠讀取到資料,非阻塞 IO 不會阻塞線程,而是直接返回 0。這種情況下,線程通過傳回值判斷資料還沒有準備好,就可以處理其他事情,而不會被阻塞。
是阻塞 IO 和非阻塞 IO 的區別,可以看出雖然 非阻塞 IO 並不會被阻塞,但是它仍然不斷的調用函數檢查資料是否已經可讀,這種現象在代碼中是以這種形式展現:
while((str = read()) == 0) {} // 繼續讀取到資料之後的邏輯。
可以明白,雖然非阻塞 IO 不會阻塞線程,但是由於沒有資料可讀,線程也沒有辦法繼續執行下面的邏輯,只能不斷的調用判斷,等待資料到來。這種情況下稱為同步 IO。所以綜上,NIO 本質上是一個非阻塞同步 IO。
IO 複用
由於 NIO 不會因為資料還沒有到達而被阻塞,那麼就沒有必要每一個 client 都分配一個 thread 不斷去輪詢判斷是否有資料可讀。可以使用一個 thread 監聽所有的 client 串連,由這個 thread 迴圈判斷是否有某個 client 的資料可讀,如果有就告知其他 thread 某個 client 串連由資料可讀。這種行為就被稱之為 IO 複用。 在 NIO 中提供了 Selector 類來監聽所有 client 串連是否有資料可讀。
使用 Selector 來實現 IO 複用,只有一個 thread 需要關心資料是否到來,其他線程等待通知就好。如此一來,只有監聽線程會一直迴圈判斷,並不會佔據太多 CPU 資源。提到 NIO 中的 Selector,不得不說一下 Linux 編程中的 IO 複用,因為 NIO 中的 Selector 底層就是使用系統級的 IO 複用方案。
Linux 系統的 IO 複用實現方案有 2 種:
在 Linux 2.6+ 的版本上 NIO 底層使用的是 epoll,在 2.4.x 的版本使用的是 select 函數。epoll 函數在效能方面比 select好很多,這裡可以不關心 Linux 編程具體細節。值得一提的是,Java 的 netty 網路架構底層就是使用 NIO 技術。
AIO
回顧一下 NIO 中:使用監聽線程調用 select 函數來監聽所有請求是否有資料到達,如果有資料則通知其他線程來讀取資料。這裡線上程讀取資料的過程中,線程在資料沒有讀取完畢之前是處於阻塞狀態,只有資料讀取完畢之後線程才可以繼續執行邏輯。之前說過,這種稱之為同步 IO。JDK 7 中新增了一套新介面 AIO(Asynchronous IO)。
AIO 有一個神奇的特性:當發起 IO 操作之後,線程不用等待 IO 讀取完畢,而是可以直接返回,繼續執行其他動作。等到資料讀取完畢之後,系統會通知線程資料已經讀取完畢。這種發起 IO 操作,但是不必等待資料讀取完畢的 IO 操作稱之為非同步 IO。如果使用 AIO,一個線程可以同時發起多個 IO 操作,這就意味著,一個線程可以同時處理多個請求。著名的 網頁伺服器 Nginx 就是用了非同步 IO。關於更多的細節,可以參考下我的另一篇文章 <Apache--MPMs && Nginx事件驅動>。
End
到目前為止,文章解釋了阻塞/非阻塞 IO,同步/非同步 IO 的區別,談起 IO 模型,不可避免會涉及 Linux 的 5 種 IO 模型
- 阻塞 IO
- 非阻塞 IO
- IO 複用
- 訊號驅動 IO
- 非同步 IO
除去訊號驅動 IO沒有提及,其他 4 種主要的 IO 模型都有所解釋,理解了這些 IO 模型的概念對於編寫代碼有很大的協助。
Java學習交流QQ群:589809992 禁止閑聊,非喜勿進!
Java 網路 IO 模型