Java NIO與IO
當學習java NIO和IO API時,大腦中會很快涌現一個問題:
什么時候用IO?什么時候用NIO?
這篇文章作者將嘗試闡明Java NIO和IO之間的一些區別、它們的用例、它們各自是如何影響我們的代碼設計的。
Java NIO和IO的主要區別
以下表格簡要說明了NIO和IO的區別,接著我們將詳細說明表格中的每個不同點。
IO | NIO |
流式(Stream oriented) | 緩沖式(Buffer oriented) |
阻塞IO | 非阻塞IO |
選擇器(Selectors) |
面向流和緩沖型
Java NIO和IO第一個大的不同點是IO是面向流的,NIO則是緩沖型的。那么,這到底是什么意思?
Java IO是面向流的意味著我們從一個流中一次讀取一個或多個字節。而要對讀到的字節作何處理由我們自己決定;這其中沒有任何緩存。此外,我們不能在數據流中來回移動;如果想要在從流讀取的數據中來回移動,我們需要首先將數據緩存到緩沖區。
Java NIO的緩沖型方法稍有不同。數據讀取到緩沖區后被加工,我們可以根據需求在數據中來回移動。這為處理提供了靈活性;然而為了充分處理所有數據我們還需要檢查緩沖區是否包含所有需要的數據,并且我們需要確保讀取更多數據到緩沖區時未被處理的數據不能被覆蓋。
阻塞型IO和非阻塞型IO
Java IO的各種流是阻塞型的。這意味著,當一個線程調用read()方法或write()方法時這個線程將一直被阻塞,直到有數據被讀到或者數據被完全寫入;在被阻塞的同時,該線程不能做任何其他事情。
Java NIO的非阻塞模式允許一個線程從一個channel中請求讀取數據,這只會取到當前有效的數據或當前沒有數據有效時獲取不到任何數據;而不是一直阻塞直到所讀取數據準備好為止;在這同時該線程可以做其他事情。
這個過程對非阻塞式數據寫入也是成立的。一個線程可以寫入一些數據到channel,但是不用等待數據被完全寫入。該線程在請求完成后可以繼續同時去做其他事情。
當線程不在IO調用上被阻塞時,那么它們的空閑時間通常都花在了在其他channel上執行IO操作。也就是說,一個線程可以管理多個輸入和輸出的channel。
選擇器(Selectors)
Java NIO中的selector允許一個線程監視多個channel的輸入。我們可以在一個selector上注冊多個channel,然后使用一個線程來“選擇”輸入可用的channel來處理,或者選擇準備好寫入的channel。這種選擇器模式使單個線程管理多個channel變的非常容易。
NIO和IO對應用設計的影響
不論我們選擇NIO還是IO作為我們的IO工具包都可能在以下幾方面影響應用的設計:
-
NIO或IO API類的調用
-
數據的處理
-
用于處理數據的線程數量
API調用
當然NIO API的調用和IO是不一樣的,這沒什么可奇怪的。與IO從流如InputStream中一個字節一個字節讀取數據不同,使用NIO時數據必須先讀到一個緩沖區中,然后再從緩沖區中處理。
數據處理
使用NIO設計和使用IO設計時數據的處理也會受影響。
IO設計中我們從InputStream或Reader中一個字節一個字節中讀取數據。假設我們要處理一個基于流的文本數據,例如:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
使用流處理這個文本代碼如下:
IInputStream input = ... ; // get theInputStream from the client socket
BufferedReader reader = newBufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();
請注意,處理狀態是有程序執行多遠決定的。換句話說,一旦第一個reader.readLine()方法返回,我們就可以知道完整的一行文本讀取完成;readLine()方法在一行讀完之前一直保持阻塞狀態,那就是原因;這一行包含name信息。相似的,當第二行readLine()方法返回,我們得到的是年齡信息,等等。
如我們所見,這個程序只有在有新數據可讀的時候才向前執行,每一步我們都知道讀到的數據是什么;一旦執行線程在代碼中向前讀取過一些數據,該線程在數據中將不能后退(絕大多數時候不能)。該規則如圖所示:
圖1:java IO 從阻塞流中讀取數據
NIO的實現則有所不同,以下是代碼例子:
ByteBuffer buffer =ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
請注意第二行從channel中往ByteBuffer中讀取字節的代碼。當該方法調用返回時我們并不知道是否我們需要的全部數據都已經在緩沖區之內了。我們所知道的就是緩沖區中包含一些字節。這使得處理在一定程度上邊的更難。
假設,如果在第一個read(buffer)調用之后讀到緩沖區的數據只有半行。例如,"Name: An"。我們能處理這個數據嗎?不能。我們需要等待直到至少一整行數據被讀到緩沖區之后,在讀取完整一行之前去處理數據是毫無意義的。
那么我們如何知道緩沖區中是否包含有足夠的數據來滿足處理要求?不知道。唯一的解決辦法就是查看緩沖區中的數據。這將導致我們需要多次檢查緩沖區中的數據來確認數據是被完全讀到緩沖區中。這種方式效率低,而且可能會導致程序設計混亂。例如:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
bufferFull()方法來跟蹤有多少數據已讀取到緩沖區,根據緩沖區是否已滿來返回true或者false。換句話說,如果緩沖區準備好可以被處理,則被認為是完整的。
雖然bufferFull()方法掃描緩沖區,但是在它被調用之前以相同的狀態離開緩沖區。如果狀態不相同,接下來讀入緩沖區的數據可能不會讀在正確的位置。這不是不可能的,但這是另一個需要注意的問題。
如果緩沖區完整,那么就可以被處理。如果不完整,在特定場景中允許的話,我們也許可以部分的處理已經在緩沖區中的數據。大多數情況下這種情況是不被允許的。
如下圖展示的是“緩沖中數據是否準備好”的邏輯:
圖2:Java NIO 從channel中讀取數據直到所有數據都存入緩沖區
總結
NIO允許使用單個(或少量)線程來管理多種channel(網絡連接或文件),但是代價是解析數據比使用阻塞流來讀取數據更復雜。
如果同時需要管理數千連接,而每個連接只是發送少量數據,比如聊天服務器,使用NIO實現則比較有優勢。相似的,如果需要保持很多連接和其他機器保持連接,如p2p網絡,使用單線程去管理所有連接也許比較合適;這中單線程、多連接的設計圖如下:
圖3:Java NIO 一個線程管理多個連接
如果非常高的帶寬下有很少連接一次性發送很多數據,那么經典的IO實現方式也許是最合適的。使用經典IO設計圖如下:
圖4:Java IO 經典IO服務器設計-一個連接由一個線程處理
1. 本文由程序員學架構翻譯
2. 本文譯自 http://java.dzone.com/articles/java-nio-vs-io
3. 轉載請務必注明本文出自:程序員學架構(微信號:archleaner )