Java NIO 學習筆記(七)----NIO/IO 的對比和總結


目錄:
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 ,可能都會影響應用程序設計的以下方面:

  1. 對 NIO 或 IO 類的API調用方式
  2. 數據的處理
  3. 用於處理數據的線程數

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”等。

所以,只有當有新數據要讀取時,程序才會進行,並且對於每個步驟,都知道該讀取的數據是什么。 一旦執行的線程已經讀取過代碼中的某個數據片段,該線程就不會再向后讀取舊數據(通常不會)。 下圖也說明了此原則:

image

同上需求,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 循環:

image

總結

NIO 允許僅使用一個(或幾個)線程來管理多個通道(網絡連接或文件),但成本是解析數據可能比從阻塞流中讀取數據時更復雜一些。

如果需要同時管理數千個打開的連接,每個只發送一些數據,例如聊天服務器,這在 NIO 中實現服務器可能是一個優勢。 同樣,如果需要與其他計算機保持大量開放連接,例如,在 P2P 網絡中,使用單個線程來管理所有出站連接可能是一個優勢。 下圖中說明了這種一個線程,多個連接的設計:

image

但如果擁有較少帶寬的連接,一次連接的數據量較大,那么經典的 IO 服務器實現可能更合適的。 下圖說明了這種典型的 IO 服務器設計:

image

所以,應該根據具體的情況分析,選擇更適合的,而不是更新的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM