Java NIO 學習筆記(七)----NIO/IO 的對比和總結
學完 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”等。
所以,只有當有新資料要讀取時,程式才會進行,並且對於每個步驟,都知道該讀取的資料是什麼。 一旦執行的執行緒已經讀取過程式碼中的某個資料片段,該執行緒就不會再向後讀取舊資料(通常不會)。 下圖也說明了此原則:

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
所以,應該根據具體的情況分析,選擇更適合的,而不是更新的。