目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----聚集和分散,通道到通道
Java NIO 學習筆記(三)----Selector
Java NIO 學習筆記(四)----文件通道和網絡通道
Java NIO 學習筆記(五)----路徑、文件和管道 Path/Files/Pipe
Java NIO 學習筆記(六)----異步文件通道 AsynchronousFileChannel
Java NIO 學習筆記(七)----NIO/IO 的對比和總結
學完 NIO 和 IO 后,有一個問題:什么時候應該使用 IO,什么時候應該使用 NIO ?本文將嘗試闡明 NIO 和 IO 之間的差異,並提供它們的用例,以及它們對程序代碼的設計影響。
NIO 和 IO 之間的主要區別
IO | NIO |
---|---|
以 Stream 為導向 | 以 Buffer 為導向 |
阻塞 IO | 非阻塞 IO 選擇器 |
以 Stream 為導向 vs 以 Buffer 為導向
NIO 和 IO 之間的第一個重要區別是 IO 是面向流的,其中 NIO 是面向緩沖區的。 那么,這意味着什么?
面向流的 IO 意味着可以從流中一次讀取一個或多個字節,可以按我們的意願使用讀取的字節。 它們不會緩存在任何地方,此外,無法在流中的將數據前后移動。 如果需要將讀取的數據前后移動,則需要先將其緩存在緩沖區中。
NIO 的面向緩沖區的方法略有不同。 將數據讀入緩沖區,稍后處理該緩沖區。 可以根據需要在緩沖區中前后移動。 這使在處理過程中更具靈活性。 但是,還需檢查該緩沖區中是否包含所有需要處理的數據,並且需要確保在將更多數據讀入緩沖區時,不會覆蓋尚未處理的緩沖區中的數據。
阻塞 IO vs 非阻塞 IO
標准 IO 的各種流都是阻塞的。 這意味着當線程調用 read() 或 write () 時,該線程將被阻塞,直到一些數據被讀取或者完全寫入,在此期間,線程無法執行任何其他操作。
NIO 的非阻塞模式允許線程請求從通道讀取數據,並且只獲取當前可用的內容,如果當前沒有數據可用,就什么都不讀取。 線程可以繼續做其他事情,而不是在數據可供讀取之前保持阻塞狀態。
非阻塞寫入也是如此。 線程可以請求將某些數據寫入通道,但在完全寫入之前不會一直等待它,這樣,線程可以在同一時間做繼續其他事情。
線程在 IO 操作中沒有因為阻塞花費等待時間,通常將等待數據准備的時間用在其他通道上執行 IO 操作。 也就是說,單個線程現在可以管理多個輸入和輸出通道。
Selector
選擇器允許單個線程監視多個輸入通道。可以使用選擇器注冊多個通道,然后使用單個線程“選擇”具有可用於處理的輸入的通道,或選擇准備寫入的通道。 這種選擇器機制使單個線程可以輕松管理多個通道。
NIO 和 IO 如何影響應用程序設計
無論選擇 NIO 還是 IO ,可能都會影響應用程序設計的以下方面:
- 對 NIO 或 IO 類的API調用方式
- 數據的處理
- 用於處理數據的線程數
API 調用方式
當然,使用 NIO 時的 API 調用看起來與使用 IO 時不同。因為必須首先將數據從通道讀入緩沖區,然后在緩沖區進行處理,而不是僅僅從 InputStream 讀取數據字節。
數據的處理
使用純 NIO 設計是,對比 IO 設計,數據處理也會受到影響。
在 IO 設計中,從 InputStream 或 Reader 中讀取字節的數據字節。 想象一下,正在處理基於行的文本數據流。 例如:
Name: czwbig
Age: 21
這組文本行可以像這樣處理:
InputStream input = ... ;
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
注意處理狀態是如何根據程序執行的程度確定的。 換句話說,一旦第一個 reader.readLine() 方法返回,就確定已經讀取了整行文本,因為 readLine() 阻塞直到讀取完整行,還知道此行包含“Name”。 同樣,當第二個 readLine() 調用返回時,可以知道此行包含“Age”等。
所以,只有當有新數據要讀取時,程序才會進行,並且對於每個步驟,都知道該讀取的數據是什么。 一旦執行的線程已經讀取過代碼中的某個數據片段,該線程就不會再向后讀取舊數據(通常不會)。 下圖也說明了此原則:
同上需求,NIO 實現看起來會有所不同。這里有一個簡化的例子:
ByteBuffer buffer = ByteBuffer.allocate(64);
int bytesRead = inChannel.read(buffer);
注意第二行從通道讀取字節到 ByteBuffer 。 當該方法調用返回時,我們是不知道所需的所有數據是否都已在緩沖區內的,只知道緩沖區包含一些字節。 這使得處理數據變得困難。
想象一下,在第一次讀取(緩沖)調用之后,是否所有讀入緩沖區的內容都是半行。 例如,“Name:cz”。 你能處理這些數據嗎? 顯然不能。 在處理任何數據之前,我們需要等待至少一整行數據進入緩沖區。
那么怎么知道緩沖區是否包含足夠的數據來處理它?唯一方法是查看緩沖區中的數據。 這樣將導致:在知道所有數據是否存在之前,可能需要多次檢查緩沖區中的數據(輪詢)。 這既低效又可能在程序設計方面變得混亂。 例如:
ByteBuffer buffer = ByteBuffer.allocate(64);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
bufferFull() 方法必須跟蹤讀入緩沖區的數據量,並返回 true 或 false ,具體取決於緩沖區是否已滿。 換句話說,如果緩沖區已准備好進行處理,則認為它已滿。
bufferFull() 方法掃描緩沖區,並且必須使緩沖區保持與調用 bufferFull() 方法之前相同的狀態。 如果不這樣,則可能無法在正確的位置繼續讀入下一個數據到緩沖區中。 這不是不可能的,但這是另一個需要注意的問題。
如果緩沖區已滿,則可以對其進行處理。 如果緩沖區還沒滿,有可能讓程序先部分處理已到達的數據,這在的特定情況下是有意義的。 但在許多情況下,不完整的數據沒有處理的意義。
這個圖中說明了 is-data-in-buffer-ready 循環:
總結
NIO 允許僅使用一個(或幾個)線程來管理多個通道(網絡連接或文件),但成本是解析數據可能比從阻塞流中讀取數據時更復雜一些。
如果需要同時管理數千個打開的連接,每個只發送一些數據,例如聊天服務器,這在 NIO 中實現服務器可能是一個優勢。 同樣,如果需要與其他計算機保持大量開放連接,例如,在 P2P 網絡中,使用單個線程來管理所有出站連接可能是一個優勢。 下圖中說明了這種一個線程,多個連接的設計:
但如果擁有較少帶寬的連接,一次連接的數據量較大,那么經典的 IO 服務器實現可能更合適的。 下圖說明了這種典型的 IO 服務器設計:
所以,應該根據具體的情況分析,選擇更適合的,而不是更新的。