在我們學(xué)習(xí) NIO 和 IO 的 API 時(shí)细疚,腦海中會(huì)冒出這個(gè)問(wèn)題:
“什么時(shí)候用 NIO?什么時(shí)候用 IO暑认?”
接下來(lái)我們將對(duì)二者之間的異同點(diǎn)進(jìn)行詳述达皿。
Java NIO 與 IO 之間的主要差異
下面的表格列出了二者間的主要差異,下文將對(duì)這些差異進(jìn)行詳述芥牌。
IO | NIO |
---|---|
面向 Stream | 面向 Buffer |
阻塞 IO | 選擇器 Selectors |
面向 Stream vs. 面向 Buffer
面向 stream 和 面向 buffer 的不同预吆,意味著什么呢?
Java IO 是面向流的胳泉,意味著你從流中一次讀取一個(gè)或多個(gè)字節(jié),你用讀到的字節(jié)做什么完全取決于你岩遗,字節(jié)沒(méi)有任何緩存扇商。此外,你無(wú)法在流中前后移動(dòng)宿礁。如果你需要在你從流中讀到的數(shù)據(jù)里前后移動(dòng)案铺,那你必須先將它們緩存在緩沖區(qū)中。
Java NIO 面向緩沖區(qū)的方法略有不同梆靖。數(shù)據(jù)被讀到之后處理的緩沖區(qū)中控汉。你可以在緩沖區(qū)中按你的需求前后移動(dòng)。這會(huì)讓你在處理期間有更大的靈活性返吻。然而姑子,為了完整的處理數(shù)據(jù),你需要檢查緩沖區(qū)是否包含了你需要的全部數(shù)據(jù)测僵。還有街佑,你需要確保往緩沖區(qū)寫(xiě)入更多數(shù)據(jù)時(shí),是否會(huì)覆蓋緩沖區(qū)中待處理的數(shù)據(jù)捍靠。
阻塞(Blocking)vs. 非阻塞(Non-blocking) IO
Java IO 的各種流都是阻塞式的沐旨。也就是說(shuō),當(dāng)一個(gè)線程調(diào)用 read() 或 write() 方法榨婆,那個(gè)線程將被阻塞磁携,直到有數(shù)據(jù)讀到或數(shù)據(jù)完全寫(xiě)入為止。該線程在此期間將什么都不做良风。
Java NIO 的非阻塞模式使一個(gè)線程能夠請(qǐng)求從通道中讀數(shù)據(jù)谊迄,僅得到當(dāng)前可讀的數(shù)據(jù)闷供,或者如果當(dāng)前沒(méi)有數(shù)據(jù)可讀時(shí),就什么都得不到鳞上。而不是一直阻塞到數(shù)據(jù)成為可供讀取的狀態(tài)这吻,在此期間線程可以繼續(xù)做其他事情。
該方式同樣適用于非阻塞式寫(xiě)入篙议。一個(gè)線程可以請(qǐng)求向通道中寫(xiě)入一些數(shù)據(jù)唾糯,但是不必等到它完全寫(xiě)入。在此期間線程可以繼續(xù)做其他事情鬼贱。
該線程在非阻塞 IO 調(diào)用期間移怯,會(huì)利用空閑時(shí)間處理其他通道的 IO 請(qǐng)求。也就是說(shuō)这难,單個(gè)線程現(xiàn)在可以管理多個(gè)通道的輸入和輸出舟误。
選擇器(Selectors)
Java NIO 的 選擇器(selectors )可以用單個(gè)線程來(lái)監(jiān)聽(tīng)多個(gè)通道(channels )的輸入狀態(tài)。你可以在一個(gè)選擇器上注冊(cè)多個(gè)通道姻乓,然后用單個(gè)線程去選中(select)那些有輸入信息的通道或準(zhǔn)備進(jìn)行輸出操作的通道來(lái)處理嵌溢。這個(gè)選擇器機(jī)制使得單線程管理多通道的問(wèn)題變得很簡(jiǎn)單。
NIO 和 IO 對(duì)應(yīng)用程序設(shè)計(jì)的影響
無(wú)論你選擇 NIO 或 IO 作為你的 IO 工具蹋岩,都可能從以下幾個(gè)方面影響應(yīng)用程序的設(shè)計(jì):
- NIO 或 IO 類(lèi)的API 調(diào)用方式赖草;
- 數(shù)據(jù)處理過(guò)程;
- 用于處理數(shù)據(jù)的線程數(shù)剪个。
API調(diào)用
采用 IO 來(lái)調(diào)用API秧骑,僅需要從輸入流(例如 InputStream)中讀取字節(jié)數(shù)據(jù),而 NIO 方式則需要先將數(shù)據(jù)讀取到一個(gè)緩沖區(qū)(Buffer)中扣囊,然后在緩沖區(qū)中進(jìn)行處理乎折。
由此來(lái)看,NIO 與 IO 在 API調(diào)用方面差距較大侵歇。
數(shù)據(jù)處理
當(dāng)使用一個(gè)純 NIO 或 IO 方式時(shí)骂澄,對(duì)數(shù)據(jù)處理過(guò)程也有一定的影響。
用 IO 方式惕虑,你可以從 InputStream 或 Reader 中讀取數(shù)據(jù)字節(jié)酗洒。假設(shè)你正在處理一個(gè)基于行的文本數(shù)據(jù)流,文本如下:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
這個(gè)文本行的流可以這樣處理:
InputStream input = ... ; // get the InputStream from the client socket
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();
程序的執(zhí)行狀態(tài)是由程序執(zhí)行了多少來(lái)決定的枷遂。換句話說(shuō)樱衷,一旦第一行 reader.readLine() 方法返回,你肯定知道已經(jīng)讀到了一整行的文本酒唉。因?yàn)?em>readLine() 會(huì)一直阻塞到整行文本讀取完成矩桂。你也知道這行文本中包含了name 信息。同理,當(dāng)?shù)诙?readLine() 調(diào)用返回時(shí)侄榴,你知道這行文本中包含了 age 信息雹锣。
正如你所看到的,只有當(dāng)有新數(shù)據(jù)讀取時(shí)癞蚕,程序才會(huì)進(jìn)行蕊爵,并且每一步你都都知道數(shù)據(jù)是什么。一旦執(zhí)行中的線程已經(jīng)執(zhí)行了讀取某個(gè)數(shù)據(jù)片段的代碼桦山,這個(gè)線程將無(wú)法(幾乎不能)回退數(shù)據(jù)攒射。這個(gè)原理在下圖中有所說(shuō)明:
NIO 的實(shí)現(xiàn)看起來(lái)會(huì)有所不同,這是一個(gè)簡(jiǎn)單的例子:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
注意第二行恒水,從通道將字節(jié)讀入到 ByteBuffer 中会放。當(dāng)方法調(diào)用返回時(shí),你不知道你所需的所有數(shù)據(jù)是否已在緩沖區(qū)中钉凌。這使得處理過(guò)程稍微更難一些咧最。
想象一下,在第一次 read(buffer) 調(diào)用之后御雕,讀入緩沖區(qū)的所有內(nèi)容都是半行矢沿。例如,"Name: An"酸纲。你能處理這些數(shù)據(jù)嗎捣鲸?顯然是不能的。你需要等到至少有一整行數(shù)據(jù)進(jìn)入到了緩沖區(qū)福青,在此之前處理任何數(shù)據(jù)都無(wú)意義。
那么你怎么知道緩沖區(qū)是否包含足夠的數(shù)據(jù)來(lái)使它有被處理的意義呢脓诡?的確无午,你不會(huì)知道。查看緩沖區(qū)中的數(shù)據(jù)是知道的唯一方法祝谚。結(jié)果是宪迟,你可能必須多次檢查緩沖區(qū)中的數(shù)據(jù),然后才知道是否所有數(shù)據(jù)都在內(nèi)部交惯。這種方式效率很低次泽,而且在程序設(shè)計(jì)方面可能變得很亂。例如:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
bufferFull() 方法必須跟蹤讀取到緩沖區(qū)中的數(shù)據(jù)量席爽,并返回 true 或 false意荤,具體取決于緩沖區(qū)是否已滿。換句話說(shuō)只锻,如果緩沖區(qū)準(zhǔn)備進(jìn)行處理玖像,則認(rèn)為緩沖區(qū)已滿。
bufferFull() 方法掃描緩沖區(qū)齐饮,但必須使緩沖區(qū)處于與調(diào)用 bufferFull() 方法之前相同的狀態(tài)捐寥。否則笤昨,讀入緩沖區(qū)的下一個(gè)數(shù)據(jù)可能不會(huì)在正確的位置讀取。這并非不可能握恳,但它也是另一個(gè)值得注意的問(wèn)題瞒窒。
如果緩沖區(qū)已滿,則可以進(jìn)行處理乡洼。如果它不是滿的崇裁,你也許能夠部分處理這些數(shù)據(jù)(假設(shè)這在你的特定情況下是有意義的,事實(shí)上在大部分情況下是沒(méi)有意義的)就珠。
在下圖中說(shuō)明了緩沖區(qū)內(nèi)數(shù)據(jù)準(zhǔn)備循環(huán)的過(guò)程:
總結(jié)
使用 NIO ,你可以用一個(gè)(或少量)線程來(lái)管理多個(gè)通道(網(wǎng)絡(luò)連接或文件)妻怎。成本是壳炎,解析數(shù)據(jù)可能比在從阻塞流讀取數(shù)據(jù)時(shí)要復(fù)雜得多。
如果你需要管理成千上萬(wàn)個(gè)同時(shí)打開(kāi)的連接逼侦,而且每個(gè)連接上只發(fā)送少量數(shù)據(jù)(比如聊天服務(wù)器)匿辩,那么采用 NIO 方式來(lái)實(shí)現(xiàn)服務(wù)器會(huì)更好。
同樣榛丢,如果你需要與其他電腦保持大量打開(kāi)的連接(例如 P2P 網(wǎng)絡(luò))铲球,使用單個(gè)線程管理所有出站(outbound)連接可能是一個(gè)優(yōu)勢(shì)。下面這個(gè)圖描述了單個(gè)線程管理多連接的設(shè)計(jì):
如果你需要少量連接數(shù)且非常高的帶寬晰赞,每次發(fā)送大量數(shù)據(jù)稼病,那么采用典型的 IO 服務(wù)器實(shí)現(xiàn)更符合需求。這個(gè)圖展示了典型的 IO 服務(wù)器設(shè)計(jì):