標籤:java io nio
當學習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: AnnaAge: 25Email: [email protected]Phone: 1234567890
使用流處理這個文本代碼如下:
IInputStream input = ... ; // get theInputStream from the client socketBufferedReader 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 )
4. 更多文章請掃碼:
Java NIO與IO