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

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

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

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