Java NIO学习系列四:NIO和IO对比
前面的一些文章中我總結(jié)了一些Java IO和NIO相關(guān)的主要知識點(diǎn),也是管中窺豹,IO類庫已經(jīng)功能很強(qiáng)大了,但是Java 為什么又要引入NIO,這是我一直不是很清楚的?前面也只是簡單提及了一下:因為性能,但是僅僅是因為性能嗎,除此之外是否還有別的原因,或者說既然NIO性能好,那為什么現(xiàn)在我們還在使用IO。本節(jié)我們就來詳細(xì)對比一下兩者的特性以及兩者之間的不一致對我們編碼所帶來的影響。
同樣,本文會主要圍繞下面幾個方面來總結(jié):?
Java NIO和IO的主要區(qū)別
NIO和IO的不同對代碼設(shè)計帶來的變化
兩種IO的各自適用場景
總結(jié)
?
1.?Java NIO和IO的主要區(qū)別
兩者之間的不同主要體現(xiàn)在如下三個方面:
- Java IO是面向流(Stream)的,而Java NIO是面向緩沖區(qū)(Buffer)的;
- IO模型的不同,Java IO是屬于阻塞式IO(Blocking IO),而Java NIO是屬于非阻塞式IO(Non Blocking IO);
- Java NIO中還引入了Selector的概念,可以實(shí)現(xiàn)多路復(fù)用;
在接下來的部分,我們逐個討論這三個不同。
1.1 面向流與面向緩沖區(qū)
Java NIO和IO之間第一個不同點(diǎn)是IO是面向流(Stream)的而NIO是面向緩沖區(qū)(Buffer)的。
Java IO是面向流的,這意味著是一次性從流中讀取一批數(shù)據(jù),這些數(shù)據(jù)并不會緩存在任何地方,并且對于在流中的數(shù)據(jù)是不支持在數(shù)據(jù)中前后移動。如果需要在這些數(shù)據(jù)中移動(為什么要移動,可以多次讀取),則還是需要將這部分?jǐn)?shù)據(jù)先緩存在緩沖區(qū)中。
而Java NIO采用的是面向緩沖區(qū)的方式,有些不同,數(shù)據(jù)會先讀取到緩沖區(qū)中以供稍后處理。在buffer中是可以方便地前移和后移,這使得在處理數(shù)據(jù)時可以有更大的靈活性。但是呢需要檢查buffer是否包含需要的所有數(shù)據(jù)以便能夠?qū)⑵渫暾靥幚?#xff0c;并且需要確保在通過channel往buffer讀數(shù)據(jù)的時候不能夠覆蓋還未處理的數(shù)據(jù)。
1.2 IO模型的區(qū)別
? Java IO中使用的流是屬于阻塞式的,意味著當(dāng)線程調(diào)用其read()或write()方法時線程會阻塞,直到完成了數(shù)據(jù)的讀寫,在讀寫的過程中線程是什么都做不了的。
Java NIO提供了一種非阻塞模式,使得線程向channel請求讀數(shù)據(jù)時,只會獲取已經(jīng)就緒的數(shù)據(jù),并不會阻塞以等待所有數(shù)據(jù)都準(zhǔn)備好(IO就是這樣做),這樣在數(shù)據(jù)準(zhǔn)備的階段線程就能夠去處理別的事情。對于非阻塞式寫數(shù)據(jù)是一樣的。線程往channel中寫數(shù)據(jù)時,并不會阻塞以等待數(shù)據(jù)寫完,而是可以處理別的事情,等到數(shù)據(jù)已經(jīng)寫好了,線程再處理這部分事情。
當(dāng)線程在進(jìn)行IO調(diào)用并且不會進(jìn)入阻塞的情況下,這部分的空余時間就可以花在和其他channel進(jìn)行IO交互上。也就是說,這樣單個線程就能夠管理多個channel的輸入和輸出了。
1.3 Selector
Java NIO中的Selector允許單個線程監(jiān)控多個channel,可以將多個channel注冊到一個Selector中,然后可以"select"出已經(jīng)準(zhǔn)備好數(shù)據(jù)的channel,或者準(zhǔn)備好寫入的channel。這個selector機(jī)制使得單個線程同時管理多個channel變得更容易。
?
2.?NIO和IO的不同對代碼設(shè)計帶來的變化
選擇使用NIO還是IO作為開發(fā)工具包會在如下幾個方面影響應(yīng)用設(shè)計:
- API是調(diào)用NIO類庫還是IO類庫;
- 數(shù)據(jù)的處理方式;
- 用來處理數(shù)據(jù)的線程的數(shù)量;
2.1 API的調(diào)用
采用NIO的API調(diào)用方式和IO是不一樣的,與直接從InputStream中讀取字節(jié)數(shù)據(jù)不同,在NIO中,數(shù)據(jù)必須要先被讀到buffer中,然后再從那里進(jìn)行后續(xù)的處理。
2.2 數(shù)據(jù)的處理方式
采用NIO的設(shè)計還是IO的設(shè)計,數(shù)據(jù)的處理方式也是不一樣的。
在IO設(shè)計中是從InputStream或Reader中逐字節(jié)讀取數(shù)據(jù)。在下面例子中,我們通過一個處理基于文本的簡單例子來說明兩種設(shè)計的區(qū)別:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
采用IO的方式,這些數(shù)據(jù)流會像下面這樣處理:
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();注意在這里處理狀態(tài)是通過程序執(zhí)行了多少就能夠確定的。換句話說,當(dāng)?shù)谝恍衦eader.readLine()返回之后,可以確定已經(jīng)讀了一整行。因為readLine()會阻塞直到整行數(shù)據(jù)讀完。而且我們能夠確切地知道所讀取的這第一行是包含名字的。類似,第二次調(diào)用readLine()返回之后我們確切地知道所讀取的內(nèi)容包含年齡。
可以知道,上面的程序只有當(dāng)有新的數(shù)據(jù)是可讀時才會進(jìn)行處理,在每一步都知道數(shù)據(jù)是什么。一旦執(zhí)行讀寫的線程已經(jīng)讀取了一些數(shù)據(jù)之后,是不能夠再返回到前面的數(shù)據(jù)(因為流的方式只能讀取一次,很好理解,像水一樣,流完了就流完了,除非你把它裝到容器里面)。上面程序中所遵循的原則如下圖所示:
而NIO的實(shí)現(xiàn)則看起來有些不同,如下:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer);注意第二行是從channel讀取數(shù)據(jù)到buffer中,當(dāng)read()方法返回時我們是不知道是否所有需要的數(shù)據(jù)有沒有全部讀到buffer中,我們知道的只是buffer中可能包含一部分?jǐn)?shù)據(jù),這會使得整個過程的處理有點(diǎn)麻煩。
假設(shè),在第一次調(diào)用read()之后,所有讀到buffer中的數(shù)據(jù)只有半行,比如,"Name:An"。這時可以處理數(shù)據(jù)嗎,顯然是不可以的(因為還沒有讀完),需要等到至少一行數(shù)據(jù)被讀到buffer中。
那么我們又如何來知道buffer中包含足夠可以處理的數(shù)據(jù)呢?唯一的辦法只有檢查buffer中的數(shù)據(jù)了。所以結(jié)果就是我們需要通過多次檢查buffer中的數(shù)據(jù)來判斷數(shù)據(jù)是否已經(jīng)全部讀進(jìn)buffer了。這樣就很低效,而且容易導(dǎo)致程序設(shè)計混亂。比如:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer); while(! bufferFull(bytesRead) ) {bytesRead = inChannel.read(buffer); }bufferFull()方法會跟蹤有多少數(shù)據(jù)被讀到buffer中了,并且返回true或者false,取決于buffer是否已滿。換言之,如果buffer中的數(shù)據(jù)已經(jīng)可供處理,那就代表它已經(jīng)滿了。
bufferFull()方法會掃描整個buffer,要保證掃描并不會影響整個buffer的狀態(tài),不然可能導(dǎo)致后面要讀入buffer中的數(shù)據(jù)不能讀到正確地位置。這并非不可能,所以對于設(shè)計者來說這是一個需要關(guān)注的地方。
如果buffer已滿,那其中的數(shù)據(jù)就可供處理。如果沒滿,那可能需要部分地處理那些數(shù)據(jù)(如果需要的話),只是在大部分場景下是不需要的。
下圖描述了這種 is-data-in-buffer-ready的循環(huán):
3.?兩種IO的各自適用場景
NIO使得通過單個或少量線程來管理多個channel(網(wǎng)絡(luò)連接或者文件)成為可能,但是代價是傳遞數(shù)據(jù)會比從阻塞的流中讀數(shù)據(jù)更復(fù)雜。我們學(xué)習(xí)一項新的技術(shù)時,既要看到其優(yōu)點(diǎn)也要看到其缺點(diǎn)。
如果需要同時管理數(shù)以千計的連接,而且每個連接只會發(fā)送少量的數(shù)據(jù),比如聊天服務(wù)器,用NIO的方式來實(shí)現(xiàn)這個服務(wù)器則比較合適。類似的,如果需要長時間保持一些和別的電腦的連接,比如在一個P2P網(wǎng)絡(luò)中,用單個線程來管理所有的對外連接也有優(yōu)勢。如下圖描述了這種單個線程,多個連接的設(shè)計模型:
如果只有少量的連接,但是每個連接又都占用大量的帶寬,短時間之內(nèi)發(fā)送大量數(shù)據(jù),這時后也許傳統(tǒng)的IO模型會更適用,因為專一,所以在特定場景下可以更高效。如下圖描述了一個基于傳統(tǒng)IO模型設(shè)計的服務(wù)器模型:
4.?總結(jié)
在前面總結(jié)了很多IO和NIO的相關(guān)知識之后,本文總結(jié)了Java中兩種IO類庫的區(qū)別即各自的優(yōu)缺點(diǎn):
- 傳統(tǒng)Java IO是面向流,從流中讀取數(shù)據(jù)或者寫入到流中,而Java NIO是面向緩沖區(qū)的,通過channel和buffer的搭配使用來讀取或者寫入數(shù)據(jù);
- 面向流只能一次讀取數(shù)據(jù);面向緩沖區(qū)可以多次讀取數(shù)據(jù);
- 面向流的方式處理數(shù)據(jù)過程相對簡單,易于實(shí)現(xiàn);而Java NIO中面向buffer的方式一般是非阻塞的方式,所以在數(shù)據(jù)的操作上會更復(fù)雜,從而會增加代碼的復(fù)雜程度;
- Java NIO提供了Selector的概念,可以通過少量線程處理多個連接,可以有效處理并發(fā);而Java IO則專注于單個線程阻塞式讀寫,對于少量連接但是每個連接都占用大量寬帶的場景更適用;
技術(shù)沒有好壞,只有合適與否!
總結(jié)
以上是生活随笔為你收集整理的Java NIO学习系列四:NIO和IO对比的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java NIO学习系列三:Select
- 下一篇: Java NIO学习系列五:I/O模型