在研究Java NIO和IO API時,很快就會想到一個問題:
什么時候應該使用IO,什么時候應該使用NIO?
在本文中,我將嘗試闡明Java NIO和IO之間的區別,它們的用例以及它們如何影響代碼的設計。 ###Java NIO和IO之間的主要區別 下表總結了Java NIO和IO之間的主要區別。 我將在表格后面的部分詳細介紹每個區別。
IO | NIO |
---|---|
面向流 | 利用緩沖區 |
阻塞IO | 非阻塞IO |
Selectors |
###流導向vs緩沖導向 Java NIO和IO之間的第一大區別是IO是面向流的,其中NIO是面向緩沖區的。 那么,這是什么意思?
面向流的Java IO意味著您一次從流中讀取一個或多個字節。 你所做的讀字節取決于你。 他們沒有被緩存在任何地方。 而且,您不能前后移動數據流。 如果您需要前后移動從流中讀取的數據,則需要先將其緩存在緩沖區中。
Java NIO的面向緩沖區的方法略有不同。 數據被讀入一個緩沖區,稍后進行處理。 您可以根據需要前后移動緩沖區。 這給你在處理過程中更多的靈活性。 但是,您還需要檢查緩沖區是否包含您需要的所有數據,以便對其進行全面處理。 而且,您需要確保在將更多數據讀入緩沖區時,不會覆蓋尚未處理的緩沖區中的數據。 ###阻塞vs非阻塞IO Java IO的各種流都被阻塞。 這意味著,當一個線程調用一個read()或write()時,該線程被阻塞,直到有一些數據要讀取,或者數據被完全寫入。 線程在此期間不能做其他事情。
Java NIO的非阻塞模式使得線程可以請求從一個通道讀取數據,并且只獲取當前可用的數據,或者根本沒有任何數據可用。 線程可以繼續使用別的東西,而不是在數據可用于讀取之前保持阻塞狀態。
無阻塞寫入也是如此。 一個線程可以請求將一些數據寫入一個通道,但不要等待它被完全寫入。 線程然后可以繼續,同時做其他事情。
線程在沒有被IO阻塞的情況下花費空閑時間,通常是在其他通道上同時執行IO。 也就是說,一個線程現在可以管理多個輸入輸出通道。
###Selectors Java NIO的選擇器允許單線程監視多個輸入通道。 您可以使用選擇器注冊多個通道,然后使用單個線程“選擇”可用于處理的輸入通道,或者選擇準備寫入的通道。 這個選擇器機制使單個線程可以輕松管理多個通道。 ###NIO和IO如何影響應用程序設計 無論您選擇NIO還是IO作為您的IO工具包,都可能會影響您應用程序設計的以下方面:
- API調用NIO或IO類。
- 數據的處理。
- 用于處理數據的線程數。 ###API調用 當然,使用NIO時的API調用看起來不同于使用IO時的調用。 這并不奇怪。 而不只是從例如數據字節讀取字節。 一個InputStream,數據必須首先被讀入一個緩沖區,然后從那里被處理。 ###數據處理 當使用純粹的NIO設計時,數據的處理也會受到IO設計的影響。
在IO設計中,您從InputStream或Reader讀取字節的數據字節。 想象一下,你正在處理一系列基于行的文本數據。 例如:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
復制代碼
這條文本行可以像這樣處理:
InputStream input = ... ; // get the InputStream from the client socketBufferedReader reader = new BufferedReader(new InputStreamReader(input));String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();
復制代碼
注意處理狀態是由程序執行的程度決定的。 換句話說,一旦第一個reader.readLine()方法返回,您肯定知道已經讀取了一整行文本。 readLine()阻塞直到讀完整行,這就是為什么。 你也知道這行包含名字。 同樣,當第二個readLine()調用返回時,您知道該行包含年齡等
正如你所看到的,只有當有新的數據要讀取時,程序才會進行,并且每一步你都知道數據是什么。 一旦正在執行的線程通過讀取代碼中的某段數據,線程就不會在數據中倒退(大部分不是)。 這個原理在這個圖中也有說明:
NIO的實現看起來不同。 這是一個簡單的例子:
ByteBuffer buffer = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buffer);
復制代碼
注意從通道讀取字節到ByteBuffer的第二行。 當該方法調用返回時,您不知道所需的所有數據是否在緩沖區內。 所有你知道的是,緩沖區包含一些字節。 這使得處理有點困難。
想象一下,如果在第一次讀取(緩沖區)調用之后,所有讀入緩沖區的內容都是半行。 例如,"Name: An"。 你能處理這些數據嗎? 不是真的。 你需要等待,直到完整的一行數據已經進入緩沖區,才有意義處理任何數據。
那么如何知道緩沖區中是否包含足夠的數據才能處理? 那么,你沒有。 唯一的辦法就是查看緩沖區中的數據。 結果是,您可能需要多次檢查緩沖區中的數據,才能知道所有數據是否在內部。 這既是低效的,而且在程序設計方面可能變得混亂。 例如:
ByteBuffer buffer = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buffer);while(! bufferFull(bytesRead) ) {bytesRead = inChannel.read(buffer);
}
復制代碼
bufferFull()方法必須跟蹤有多少數據被讀入緩沖區,并根據緩沖區是否已滿而返回true或false。 換句話說,如果緩沖區準備好處理,則認為已滿。
bufferFull()方法掃描緩沖區,但必須保持緩沖區處于與調用bufferFull()方法之前相同的狀態。 否則,讀入緩沖區的下一個數據可能無法在正確的位置讀入。 這不是不可能的,但這是另一個需要注意的問題。
如果緩沖區已滿,可以進行處理。 如果它沒有滿,那么你可能能夠部分處理任何數據,如果這在你的特定情況下是有意義的。 在許多情況下,它不是。
圖中顯示了is-data-in-buffer-ready循環:
如果你需要管理數以千計的同時打開的連接,每一個只發送一點點的數據,例如聊天服務器,在NIO中實現服務器可能是一個優勢。 同樣,如果你需要保持與其他計算機的大量開放連接,例如 在P2P網絡中,使用單個線程來管理所有出站連接可能是一個優勢。 這一個線程,多個連接設計如下圖所示:
如果您的連接帶寬非常高,一次發送大量數據,那么傳統的IO服務器實現可能是最合適的。 該圖說明了一個經典的IO服務器設計: