18. NIO與IO的區(qū)別
??NIO即New IO,這個(gè)庫(kù)是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但實(shí)現(xiàn)方式不同,NIO主要用到的是塊,所以NIO的效率要比IO高很多。在Java API中提供了兩套NIO,一套是針對(duì)標(biāo)準(zhǔn)輸入輸出NIO,另一套就是網(wǎng)絡(luò)編程N(yùn)IO。
??NIO和IO的主要區(qū)別,下表總結(jié)了Java IO和NIO之間的主要區(qū)別:
| IO | NIO |
|---|---|
| 面向流 | 面向緩沖 |
| 阻塞IO | 非阻塞IO |
| 無(wú) | 選擇器 |
1、面向流與面向緩沖
??Java IO和NIO之間第一個(gè)最大的區(qū)別是,IO是面向流的,NIO是面向緩沖區(qū)的。 Java IO面向流意味著每次從流中讀一個(gè)或多個(gè)字節(jié),直至讀取所有字節(jié),它們沒(méi)有被緩存在任何地方。此外,它不能前后移動(dòng)流中的數(shù)據(jù)。如果需要前后移動(dòng)從流中讀取的數(shù)據(jù),需要先將它緩存到一個(gè)緩沖區(qū)。Java NIO的緩沖導(dǎo)向方法略有不同。數(shù)據(jù)讀取到一個(gè)它稍后處理的緩沖區(qū),需要時(shí)可在緩沖區(qū)中前后移動(dòng)。這就增加了處理過(guò)程中的靈活性。但是,還需要檢查是否該緩沖區(qū)中包含所有您需要處理的數(shù)據(jù)。而且,需確保當(dāng)更多的數(shù)據(jù)讀入緩沖區(qū)時(shí),不要覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)。
2、阻塞與非阻塞IO
??Java IO的各種流是阻塞的。這意味著,當(dāng)一個(gè)線程調(diào)用read() 或 write() 時(shí),該線程被阻塞,直到有一些數(shù)據(jù)被讀取,或數(shù)據(jù)完全寫(xiě)入。該線程在此期間不能再干任何事情了。Java NIO的非阻塞模式,使一個(gè)線程從某通道發(fā)送請(qǐng)求讀取數(shù)據(jù),但是它僅能得到目前可用的數(shù)據(jù),如果目前沒(méi)有數(shù)據(jù)可用時(shí),就什么都不會(huì)獲取,而不是保持線程阻塞,所以直至數(shù)據(jù)變的可以讀取之前,該線程可以繼續(xù)做其他的事情。非阻塞寫(xiě)也是如此。一個(gè)線程請(qǐng)求寫(xiě)入一些數(shù)據(jù)到某通道,但不需要等待它完全寫(xiě)入,這個(gè)線程同時(shí)可以去做別的事情。線程通常將非阻塞IO的空閑時(shí)間用于在其它通道上執(zhí)行IO操作,所以一個(gè)單獨(dú)的線程現(xiàn)在可以管理多個(gè)輸入和輸出通道(channel)。
3、選擇器(Selectors)
??Java NIO的選擇器允許一個(gè)單獨(dú)的線程來(lái)監(jiān)視多個(gè)輸入通道,你可以注冊(cè)多個(gè)通道使用一個(gè)選擇器,然后使用一個(gè)單獨(dú)的線程來(lái)“選擇”通道:這些通道里已經(jīng)有可以處理的輸入,或者選擇已準(zhǔn)備寫(xiě)入的通道。這種選擇機(jī)制,使得一個(gè)單獨(dú)的線程很容易來(lái)管理多個(gè)通道。
18.1、NIO和IO適用場(chǎng)景
??NIO是為彌補(bǔ)傳統(tǒng)IO的不足而誕生的,但是尺有所短寸有所長(zhǎng),NIO也有缺點(diǎn),因?yàn)镹IO是面向緩沖區(qū)的操作,每一次的數(shù)據(jù)處理都是對(duì)緩沖區(qū)進(jìn)行的,那么就會(huì)有一個(gè)問(wèn)題,在數(shù)據(jù)處理之前必須要判斷緩沖區(qū)的數(shù)據(jù)是否完整或者已經(jīng)讀取完畢,如果沒(méi)有,假設(shè)數(shù)據(jù)只讀取了一部分,那么對(duì)不完整的數(shù)據(jù)處理沒(méi)有任何意義。所以每次數(shù)據(jù)處理之前都要檢測(cè)緩沖區(qū)數(shù)據(jù)。
??那么NIO和IO各適用的場(chǎng)景是什么呢?
??如果需要管理同時(shí)打開(kāi)的成千上萬(wàn)個(gè)連接,這些連接每次只是發(fā)送少量的數(shù)據(jù),例如聊天服務(wù)器,這時(shí)候用NIO處理數(shù)據(jù)可能是個(gè)很好的選擇。
??而如果只有少量的連接,而這些連接每次要發(fā)送大量的數(shù)據(jù),這時(shí)候傳統(tǒng)的IO更合適。使用哪種處理數(shù)據(jù),需要在數(shù)據(jù)的響應(yīng)等待時(shí)間和檢查緩沖區(qū)數(shù)據(jù)的時(shí)間上作比較來(lái)權(quán)衡選擇。
18.2、Java NIO 總覽
??Java NIO的三個(gè)核心基礎(chǔ)組件,Channels、Buffers、Selectors。其余的諸如Pipe,F(xiàn)ileLcok都是在使用以上三個(gè)核心組件時(shí)幫助更好使用的工具類。
一、Channels和Buffers的關(guān)系
??所有的IO操作在NIO中都是以Channel開(kāi)始的。一個(gè)Channel就像一個(gè)流,NIO Channel和流很近似但是也有一些不同。
??1)、你既可以讀取也可以寫(xiě)入到Channel,流只能讀取或者寫(xiě)入,inputStream和outputStream。
??2)、Channel可以異步地讀和寫(xiě)。
??3)、channel永遠(yuǎn)都是從一個(gè)buffer中讀或者寫(xiě)入到一個(gè)buffer中去。

??基本的Channel實(shí)現(xiàn)有以下這些:
??1)、FileChannel:向文件當(dāng)中讀寫(xiě)數(shù)據(jù);
??2)、DatagramChannel:通過(guò)UDP協(xié)議向網(wǎng)絡(luò)讀寫(xiě)數(shù)據(jù);
??3)、SocketChannel:通過(guò)TCP協(xié)議向網(wǎng)絡(luò)讀寫(xiě)數(shù)據(jù);
??4)、ServerSocketChannel:以一個(gè)web服務(wù)器的形式,監(jiān)聽(tīng)到來(lái)的TCP連接,對(duì)每個(gè)連接建立一個(gè)SocketChannel。
??涵蓋了UDP,TCP以及文件的IO操作。
??核心的buffer實(shí)現(xiàn)有這些:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer,涵蓋了所有的基本數(shù)據(jù)類型(4類8種,除了Boolean)。也有其他的buffer如MappedByteBuffer。
一個(gè)簡(jiǎn)單的channel例子:使用一個(gè)FileChannel將數(shù)據(jù)讀入一個(gè)buffer。
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
buf.flip()的意思是讀寫(xiě)轉(zhuǎn)換,首先你讀入一個(gè)buffer,然后你flip,轉(zhuǎn)換讀寫(xiě),然后再?gòu)腷uffer中讀出。
二、NIO buffer
??NIO buffer在與NIO Channel交互時(shí)使用,數(shù)據(jù)從Channel中讀取出來(lái)放入buffer,或者從buffer中讀取出來(lái)寫(xiě)入Channel。
??buffer就是一塊內(nèi)存,你可以寫(xiě)入數(shù)據(jù),并且在之后讀取它。這塊內(nèi)存被包裝成NIO buffer對(duì)象,它提供了一些方法來(lái)讓你更簡(jiǎn)單地操作內(nèi)存。
??buffer的基本使用,使用buffer讀寫(xiě)數(shù)據(jù)基本上分為以下4部操作:
??1)、將數(shù)據(jù)寫(xiě)入buffer
??2)、調(diào)用buffer.flip()
??3)、將數(shù)據(jù)從buffer中讀取出來(lái)
??4)、調(diào)用buffer.clear()或者buffer.compact()
??在寫(xiě)buffer的時(shí)候,buffer會(huì)跟蹤寫(xiě)入了多少數(shù)據(jù),需要讀buffer的時(shí)候,需要調(diào)用flip()來(lái)將buffer從寫(xiě)模式切換成讀模式,讀模式中只能讀取寫(xiě)入的數(shù)據(jù),而非整個(gè)buffer。
??當(dāng)數(shù)據(jù)都讀完了,你需要清空buffer以供下次使用,可以有2種方法來(lái)操作:調(diào)用clear() 或者 調(diào)用compact()。
??區(qū)別:clear方法清空整個(gè)buffer,compact方法只清除你已經(jīng)讀取的數(shù)據(jù),未讀取的數(shù)據(jù)會(huì)被移到buffer的開(kāi)頭,此時(shí)寫(xiě)入數(shù)據(jù)會(huì)從當(dāng)前數(shù)據(jù)的末尾開(kāi)始。
// 創(chuàng)建一個(gè)容量為48的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
// 從channel中讀(取數(shù)據(jù)然后寫(xiě))入buffer
int bytesRead = inChannel.read(buf);
// 下面是讀取buffer
while (bytesRead != -1) {
buf.flip(); // 轉(zhuǎn)換buffer為讀模式
System.out.print((char) buf.get()); // 一次讀取一個(gè)byte
buf.clear(); //清空buffer準(zhǔn)備下一次寫(xiě)入
}
1、buffer的Capacity,Position和Limit
??buffer有3個(gè)屬性需要熟悉以理解buffer的工作原理:
??容量(Capacity):緩沖區(qū)能夠容納的數(shù)據(jù)元素的最大數(shù)量。容量在緩沖區(qū)創(chuàng)建時(shí)被設(shè)定,并且永遠(yuǎn)不能被改變。
??上界(Limit):寫(xiě)模式中等價(jià)于buffer的大小,即capacity;讀模式中為當(dāng)前緩沖區(qū)中一共有多少數(shù)據(jù),即可讀的最大位置。這意味著當(dāng)調(diào)用filp()方法切換成讀模式時(shí),limit的值變成position的值,而position重新指向0。
??位置(Position):下一個(gè)要被讀或?qū)懙脑氐奈恢谩3跏蓟癁?,buffer滿時(shí),position最大值為capacity-1。切換成讀模式的時(shí)候,position指向0。Position會(huì)自動(dòng)由相應(yīng)的 get( )和 put( )函數(shù)更新。
??position和limit的值在讀/寫(xiě)模式中是不一樣的。capacity的值永遠(yuǎn)表示buffer的大小。
??下圖解釋了在讀/寫(xiě)模式中Capacity,Position和Limit的意思。
??

2、創(chuàng)建一個(gè)buffer
??獲得一個(gè)buffer 之前必須先分配一塊內(nèi)存,每個(gè)buffer類都有一個(gè)靜態(tài)方法allocate() 來(lái)做這件事。
??下例為創(chuàng)建一個(gè)容量為48byte的ByteBuffer:
??ByteBuffer buf = ByteBuffer.allocate(48);
??創(chuàng)建一個(gè)1024個(gè)字符的CharBuffer
??CharBuffer buf = CharBuffer.allocate(1024);
3、將數(shù)據(jù)寫(xiě)入buffer
??寫(xiě)入buffer的方法有2種:
????1)、從一個(gè)Channel中寫(xiě)入buffer。
????2)、調(diào)用buffer的put()方法來(lái)自行寫(xiě)入數(shù)據(jù)。
??例:
??int bytesRead = inChannel.read(buf); // 從channel讀入buffer
??buf.put(127); // 自行寫(xiě)入buffer
??put方法有很多的重載形式。以供你用各種不同的方法寫(xiě)入buffer中,比如從一個(gè)特定的position,或者寫(xiě)入一個(gè)array。
4、flip()
??flip方法將寫(xiě)模式切換成讀模式,調(diào)用flip()方法會(huì)將limit設(shè)置為position,將position設(shè)置回0。
??換句話說(shuō),position標(biāo)志著寫(xiě)模式中寫(xiě)到哪里,切換成讀模式之后,limit標(biāo)志著之前寫(xiě)到哪里,也就是現(xiàn)在能讀到哪里。
5、從buffer中讀取數(shù)據(jù)
??有2種方法可以從buffer中讀取數(shù)據(jù)。
????1)、從buffer中讀取數(shù)據(jù)到channel中。
????2)、使用buffer的get()方法自行從buffer中讀出數(shù)據(jù)。
??例子:
??// 從buffer中讀取數(shù)據(jù)到channel中
??int bytesWritten = inChannel.write(buf);
??// 使用buffer的get()方法自行從buffer中讀出數(shù)據(jù)
??byte aByte = buf.get();
??get方法有很多的重載形式。以供你用各種不同的方法讀取buffer中的數(shù)據(jù)。例如從特定位置讀取數(shù)據(jù),或者讀一個(gè)數(shù)組出來(lái)。
6、rewind()
??rewind()方法將position設(shè)置為0,但是不會(huì)動(dòng)buffer里的數(shù)據(jù),這樣可以從頭開(kāi)始重新讀取數(shù)據(jù),limit的值不會(huì)變,這意味著limit依舊標(biāo)志著能讀多少數(shù)據(jù)。
7、clear()和compact()
??當(dāng)你讀完所有的數(shù)據(jù)想要重新寫(xiě)入數(shù)據(jù)時(shí),你可以調(diào)用clear或者compact方法。
??當(dāng)你調(diào)用clear()方法的時(shí)候,position被設(shè)置為0,limit被設(shè)置為capacity,換句話說(shuō),buffer的數(shù)據(jù)雖然都還在,但是buffer被初始化了,處于可以被重寫(xiě)的狀態(tài)。這也就意味著如果buffer中還有沒(méi)被讀取的數(shù)據(jù),在執(zhí)行clear之后,你無(wú)法知道數(shù)據(jù)讀到哪兒了,剩下的數(shù)據(jù)還有多少。
??如果還有沒(méi)有讀完的數(shù)據(jù),但是你想先寫(xiě)數(shù)據(jù),可以用compact()方法,這樣未讀數(shù)據(jù)會(huì)放在buffer前端,可以在未讀數(shù)據(jù)之后跟著寫(xiě)新的數(shù)據(jù)。compact()會(huì)復(fù)制未讀數(shù)據(jù)到buffer前端,然后設(shè)置position為未讀數(shù)據(jù)單位后面緊跟的位置。limit還是設(shè)置為capacity,這和clear是一樣的。現(xiàn)在buffer處于可以寫(xiě)的狀態(tài),但是不會(huì)覆蓋之前未讀完的數(shù)據(jù)。
8、mark()和reset()
??你可以通過(guò)調(diào)用buffer.mark()來(lái)mark一個(gè)buffer中給定的位置。然后你就可以用buffer.reset()方法來(lái)將position設(shè)置回之前mark的位置。
??例子:
??buffer.mark();
??// 調(diào)用buffer.get()方法若干次,e.g. 比如在做parsing的時(shí)候
??buffer.reset(); //set position back to mark.
9、equals() 和 compareTo()
??使用這2種方法能夠比較2個(gè)buffer。
??equals()方法:用于判斷2個(gè)buffer是否相等,2個(gè)buffer是equal的,當(dāng)它們:
??1)、是同一種數(shù)據(jù)類型的buffer。
??2)、buffer中未讀取的bytes,chars等數(shù)據(jù)個(gè)數(shù)是一樣的,即(limit-position)相等,capacity不需要相等,剩余數(shù)據(jù)的索引也不需要相等。
??3)、未讀取的bytes,chars等內(nèi)容是一模一樣的,即各自[position,limit-1]索引的數(shù)據(jù)要完全相等。
??如你所見(jiàn),equals()方法只比較buffer的部分內(nèi)容,而不是buffer中所有的數(shù)據(jù),事實(shí)上,它只比較buffer中剩余的元素是否一樣。
compareTo()
??compareTo()方法:比較兩個(gè)buffer的剩余元素(字節(jié),字符等),用于例如: 排序。
??在下列情況下,緩沖區(qū)被認(rèn)為比另一個(gè)緩沖區(qū)“小”:
??比較是針對(duì)每個(gè)緩沖區(qū)你剩余數(shù)據(jù)(從 position 到 limit)進(jìn)行的,與它們?cè)?equals() 中的方式相同,直到不相等的元素被發(fā)現(xiàn)或者到達(dá)緩沖區(qū)的上界。如果一個(gè)緩沖區(qū)在不相等元素發(fā)現(xiàn)前已經(jīng)被耗盡,較短的緩沖區(qū)被認(rèn)為是小于較長(zhǎng)的緩沖區(qū)。
三、NIO Selectors
??Selector允許一個(gè)線程來(lái)監(jiān)視多個(gè)Channel,這在當(dāng)你的應(yīng)用建立了多個(gè)連接,但是每個(gè)連接吞吐量都較小的時(shí)候是可行的。例如:一個(gè)聊天服務(wù)器。圖為一個(gè)線程使用Selector處理三個(gè)Channel。

??要使用一個(gè)Selector,你要先注冊(cè)這個(gè)Selector的Channels。然后你調(diào)用Selector的select()方法。這個(gè)方法會(huì)阻塞,直到它注冊(cè)的Channels當(dāng)中有一個(gè)準(zhǔn)備好了的事件發(fā)生了。當(dāng)select()方法返回的時(shí)候,線程可以處理這些事件,如新的連接的到來(lái),數(shù)據(jù)收到了等。