目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----彙集和分散,通道到通道
Java NIO 學習筆記(三)----Selector
Java NIO 學習筆記(四)----文件通道和網絡通道
Java NIO 學習筆記(五)----路徑、文件和管道 Path/Files/Pipe
Java NIO 學習筆記(六)----異步文件通道 AsynchronousFileChannel
Java NIO 學習筆記(七)----NIO/IO 的對比和總結html
Java NIO (來自 Java 1.4)能夠替代標準 IO 和 Java Networking API ,NIO 提供了與標準 IO 不一樣的使用方式。學習 NIO 以前建議先掌握標準 IO 和 Java 網絡編程,推薦教程:java
本文目的: 掌握了標準 IO 以後繼續學習 NIO 知識。主要參考 JavaDoc 和 Jakob Jenkov 的英文教程 Java NIO Tutorial編程
NIO 由如下核心組件組成:數組
通道和緩衝區
在標準 IO API 中,使用字節流和字符流。 在 NIO 中使用通道和緩衝區。 數據老是從通道讀入緩衝區,或從緩衝區寫入通道。服務器
非阻塞IO
NIO 能夠執行非阻塞 IO 。 例如,當通道將數據讀入緩衝區時,線程能夠執行其餘操做。 而且一旦數據被讀入緩衝區,線程就能夠繼續處理它。 將數據寫入通道也是如此。網絡
選擇器
NIO 包含「選擇器」的概念。 選擇器是一個能夠監視多個事件通道的對象(例如:鏈接打開,數據到達等)。 所以,單個線程能夠監視多個通道的數據。app
NIO 有比這些更多的類和組件,但在我看來,Channel,Buffer 和 Selector 構成了 API 的核心。 其他的組件,如 Pipe 和 FileLock ,只是與三個核心組件一塊兒使用的實用程序類。dom
一般,NIO 中的全部 IO 都以 Channel 開頭,頻道有點像流。 數據能夠從 Channel 讀入 Buffer,也能夠從 Buffer 寫入 Channel :
異步
有幾種 Channel 和 Buffer ,如下是 NIO 中主要 Channel 實現類的列表,這些通道包括 UDP + TCP 網絡 IO 和文件 IO:學習
這些類也有一些有趣的接口,但爲了簡單起見,這裏暫時不提,後續會進行學習的。
如下是 NIO 中的核心 Buffer 實現,其實就是 7 種基本類型:
NIO 還有一個 MappedByteBuffer,它與內存映射文件一塊兒使用,一樣這個後續再講。
選擇器容許單個線程處理多個通道。 若是程序打開了許多鏈接(通道),但每一個鏈接只有較低的流量,使用選擇器就很方便。 例如,在聊天服務器中, 如下是使用 Selector 處理 3 個 Channel 的線程圖示:
要使用選擇器,須要使用它註冊通道。 而後你調用它的 select() 方法。 此方法將阻塞,直到有一個已註冊通道的事件準備就緒。 一旦該方法返回,該線程就能夠處理這些事件。 事件能夠是傳入鏈接,接收數據等。
NIO 通道相似於流,但有一些區別:
如上所述,NIO 中老是將數據從通道讀取到緩衝區,或將數據從緩衝區寫入通道。 這是一個例子:
// 文件內容是 123456789 RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw"); FileChannel fileChannel = accessFile.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(48); int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區,返回讀入到緩衝區的字節數
使用 Buffer 與 Channel 交互,數據從通道讀入緩衝區,或從緩衝區寫入通道。
緩衝區本質上是一個能夠寫入數據的內存塊,以後能夠讀取數據。 Buffer 對象包裝了此內存塊,提供了一組方法,能夠更輕鬆地使用內存塊。
使用 Buffer 讀取和寫入數據一般遵循如下四個步驟:
將數據寫入Buffer 時,Buffer 會跟蹤寫入的數據量。 當須要讀取數據時,就使用 flip() 方法將緩衝區從寫入模式切換到讀取模式。 在讀取模式下,緩衝區容許讀取寫入緩衝區的全部數據。
讀完全部數據以後,就須要清除緩衝區,以便再次寫入。 能夠經過兩種方式執行此操做:經過調用 clear() 或調用 compact() 。區別在於 clear() 是方法清除整個緩衝區,而 compact() 方法僅清除已讀取的數據,未讀數據都會移動到緩衝區的開頭,新數據將在未讀數據以後寫入緩衝區。
這是一個簡單的緩衝區用法示例:
public class ChannelExample { public static void main(String[] args) throws IOException { // 文件內容是 123456789 RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw"); FileChannel fileChannel = accessFile.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(48); //建立容量爲48字節的緩衝區 int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區,返回讀入到緩衝區的字節數 while (data != -1) { System.out.println("Read " + data); // Read 9 buffer.flip(); // 將 buffer 從寫入模式切換爲讀取模式 while (buffer.hasRemaining()) { System.out.print((char) buffer.get()); // 每次讀取1byte,循環輸出 123456789 } buffer.clear(); // 清除當前緩衝區 data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區 } accessFile.close(); } }
緩衝區有 3 個須要熟悉的屬性,以便了解緩衝區的工做原理。 這些是:
另外還有標記 mark ,
標記、位置、限制和容量值遵照如下不變式:
0 <= mark<= position <= limit<= capacity
position 和 limit 的含義取決於 Buffer 是處於讀取仍是寫入模式。 不管緩衝模式如何,capacity 老是同樣的表示容量。
如下是寫入和讀取模式下的容量,位置和限制的說明:
做爲存儲器塊,緩衝區具備必定的固定大小,也稱爲「容量」。 只能將 capacity 多的 byte,long,char 等寫入緩衝區。 緩衝區已滿後,須要清空它(讀取數據或清除它),而後才能將更多數據寫入。
將數據寫入緩衝區時,能夠在某個位置執行操做。 position 初始值爲 0 ,當一個 byte,long,char 等已寫入緩衝區時,position 被移動,指向緩衝區中的下一個單元以插入數據。 position 最大值爲 capacity -1
從緩衝區讀取數據時,也能夠從給定位置開始讀取數據。 當緩衝區從寫入模式切換到讀取模式時,position 將重置爲 0 。當從緩衝區讀取數據時,將從 position 位置開始讀取數據,讀取後會將 position 移動到下一個要讀取的位置。
在寫入模式下,Buffer 的 limit 是能夠寫入緩衝區的數據量的限制,此時 limit=capacity。
將緩衝區切換爲讀取模式時,limit 表示最多能讀到多少數據。 所以,當將 Buffer 切換到讀取模式時,limit被設置爲以前寫入模式的寫入位置(position ),換句話說,你能讀到以前寫入的全部數據(例如以前寫寫入了 6 個字節,此時 position=6 ,而後切換到讀取模式,limit 表明最多能讀取的字節數,所以 limit 也等於 6)。
要獲取 Buffer 對象,必須先分配它。 每一個 Buffer 類都有一個 allocate() 方法來執行此操做。 下面是一個顯示ByteBuffer分配的示例,容量爲48字節:
ByteBuffer buffer = ByteBuffer.allocate(48); //建立容量爲48字節的緩衝區
能夠經過兩種方式將數據寫入 Buffer:
這是一個示例,顯示了 Channel 如何將數據寫入 Buffer:
int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區,返回讀入到緩衝區的字節數 buffer.put(127); // 此處的 127 是 byte 類型
put() 方法有許多其餘版本,容許以多種不一樣方式將數據寫入 Buffer 。 例如,在特定位置寫入,或將一個字節數組寫入緩衝區。
flip() 方法將 Buffer 從寫入模式切換到讀取模式。 調用 flip() 會將 position 設置回 0,並將 limit 的值設置爲切換以前的 position 值。換句話說,limit 表示以前寫進了多少個 byte、char 等 —— 如今能讀取多少個 byte、char 等。
有兩種方法能夠從 Buffer 中讀取數據:
如下是將緩衝區中的數據讀入通道的示例:
int bytesWritten = fileChannel.write(buffer); byte aByte = buffer.get();
和 put() 方法同樣,get() 方法也有許多其餘版本,容許以多種不一樣方式從 Buffer 中讀取數據。有關更多詳細信息,請參閱JavaDoc以獲取具體的緩衝區實現。
如下列出 ByteBuffer 類的部分方法:
方法 | 描述 |
---|---|
byte[] array() | 返回實現此緩衝區的 byte 數組,此緩衝區的內容修改將致使返回的數組內容修改,反之亦然。 |
CharBuffer asCharBuffer() | 建立此字節緩衝區做爲新的獨立的char 緩衝區。新緩衝區的內容將今後緩衝區的當前位置開始 |
XxxBuffer asXxxBuffer() | 同上,建立對應的 Xxx 緩衝區,Xxx 可爲 Short/Int/Long/Float/Double |
byte get() | 相對 get 方法。讀取此緩衝區當前位置的字節,而後該 position 遞增。 |
ByteBuffer get(byte[] dst, int offset, int length) | 相對批量 get 方法,後2個參數可省略 |
byte get(int index) | 絕對 get 方法。讀取指定索引處的字節。 |
char getChar() | 用於讀取 char 值的相對 get 方法。 |
char getChar(int index) | 用於讀取 char 值的絕對 get 方法。 |
xxx getXxx(int index) | 用於讀取 xxx 值的絕對 get 方法。index 能夠選,指定位置。 |
衆多 put() 方法 | 參考以上 get() 方法 |
static ByteBuffer wrap(byte[] array) | 將 byte 數組包裝到緩衝區中。 |
Buffer對象的 rewind() 方法將 position 設置回 0,所以能夠重讀緩衝區中的全部數據, limit 則保持不變。
若是調用 clear() ,則將 position 設置回 0 ,並將 limit 被設置成 capacity 的值。換句話說,Buffer 被清空了。 可是 Buffer 中的實際存放的數據並未清除。
若是在調用 clear() 時緩衝區中有任何未讀數據,數據將被「遺忘」,這意味着再也不有任何標記告訴讀取了哪些數據,尚未讀取哪些數據。
若是緩衝區中仍有未讀數據,而且想稍後讀取它,但須要先寫入一些數據,這時候應該調用 compact() ,它會將全部未讀數據複製到 Buffer 的開頭,而後它將 position 設置在最後一個未讀元素以後。 limit 屬性仍設置爲 capacity ,就像 clear() 同樣。 如今緩衝區已準備好寫入,而且不會覆蓋未讀數據。
以經過調用 Buffer 對象的 mark() 方法在 Buffer 中標記給定位置。 而後,能夠經過調用 Buffer.reset() 方法將位置重置回標記位置,就像在標準 IO 中同樣。
buffer.mark(); // 調用 buffer.get() 等方法讀取數據... buffer.reset(); // 設置 position 回到 mark 位置。
可使用 equals() 和 compareTo() 比較兩個緩衝區。
equals() 成立的條件:
如上,equals 僅比較緩衝區的一部分,而不是它內部的每一個元素。 實際上,它只是比較緩衝區中的其他元素。
compareTo() 方法比較兩個緩衝區的剩餘元素(字節,字符等), 在下列狀況下,一個 Buffer 被視爲「小於」另外一個 Buffer: