目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----聚集和分散,通道到通道
Java NIO 學習筆記(三)----Selector
Java NIO 學習筆記(四)----文件通道和網絡通道
Java NIO 學習筆記(五)----路徑、文件和管道 Path/Files/Pipe
Java NIO 學習筆記(六)----異步文件通道 AsynchronousFileChannel
Java NIO 學習筆記(七)----NIO/IO 的對比和總結
Java NIO (來自 Java 1.4)可以替代標准 IO 和 Java Networking API ,NIO 提供了與標准 IO 不同的使用方式。學習 NIO 之前建議先掌握標准 IO 和 Java 網絡編程,推薦教程:
本文目的: 掌握了標准 IO 之后繼續學習 NIO 知識。主要參考 JavaDoc 和 Jakob Jenkov 的英文教程 Java NIO Tutorial
Java NIO 概覽
NIO 由以下核心組件組成:
-
通道和緩沖區
在標准 IO API 中,使用字節流和字符流。 在 NIO 中使用通道和緩沖區。 數據總是從通道讀入緩沖區,或從緩沖區寫入通道。 -
非阻塞IO
NIO 可以執行非阻塞 IO 。 例如,當通道將數據讀入緩沖區時,線程可以執行其他操作。 並且一旦數據被讀入緩沖區,線程就可以繼續處理它。 將數據寫入通道也是如此。 -
選擇器
NIO 包含“選擇器”的概念。 選擇器是一個可以監視多個事件通道的對象(例如:連接打開,數據到達等)。 因此,單個線程可以監視多個通道的數據。
NIO 有比這些更多的類和組件,但在我看來,Channel,Buffer 和 Selector 構成了 API 的核心。 其余的組件,如 Pipe 和 FileLock ,只是與三個核心組件一起使用的實用程序類。
Channels/Buffers 通道和緩沖區
通常,NIO 中的所有 IO 都以 Channel 開頭,頻道有點像流。 數據可以從 Channel 讀入 Buffer,也可以從 Buffer 寫入 Channel :
有幾種 Channel 和 Buffer ,以下是 NIO 中主要 Channel 實現類的列表,這些通道包括 UDP + TCP 網絡 IO 和文件 IO:
- FileChannel :文件通道
- DatagramChannel :數據報通道
- SocketChannel :套接字通道
- ServerSocketChannel :服務器套接字通道
這些類也有一些有趣的接口,但為了簡單起見,這里暫時不提,后續會進行學習的。
以下是 NIO 中的核心 Buffer 實現,其實就是 7 種基本類型:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
NIO 還有一個 MappedByteBuffer,它與內存映射文件一起使用,同樣這個后續再講。
Selectors 選擇器
選擇器允許單個線程處理多個通道。 如果程序打開了許多連接(通道),但每個連接只有較低的流量,使用選擇器就很方便。 例如,在聊天服務器中, 以下是使用 Selector 處理 3 個 Channel 的線程圖示:
要使用選擇器,需要使用它注冊通道。 然后你調用它的 select() 方法。 此方法將阻塞,直到有一個已注冊通道的事件准備就緒。 一旦該方法返回,該線程就可以處理這些事件。 事件可以是傳入連接,接收數據等。
Channel (通道)
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(緩沖區)
使用 Buffer 與 Channel 交互,數據從通道讀入緩沖區,或從緩沖區寫入通道。
緩沖區本質上是一個可以寫入數據的內存塊,之后可以讀取數據。 Buffer 對象包裝了此內存塊,提供了一組方法,可以更輕松地使用內存塊。
Buffer 的基本用法
使用 Buffer 讀取和寫入數據通常遵循以下四個步驟:
- 將數據寫入緩沖區
- 調用 buffer.flip() 反轉讀寫模式
- 從緩沖區讀取數據
- 調用 buffer.clear() 或 buffer.compact() 清除緩沖區內容
將數據寫入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();
}
}
Buffer 的 capacity,position 和 limit
緩沖區有 3 個需要熟悉的屬性,以便了解緩沖區的工作原理。 這些是:
- capacity : 容量緩沖區的容量,是它所包含的元素的數量。不能為負並且不能更改。
- position :緩沖區的位置 是下一個要讀取或寫入的元素的索引。不能為負,並且不能大於 limit
- limit : 緩沖區的限制,緩沖區的限制不能為負,並且不能大於 capacity
另外還有標記 mark ,
標記、位置、限制和容量值遵守以下不變式:
0 <= mark<= position <= limit<= capacity
position 和 limit 的含義取決於 Buffer 是處於讀取還是寫入模式。 無論緩沖模式如何,capacity 總是一樣的表示容量。
以下是寫入和讀取模式下的容量,位置和限制的說明:
capacity
作為存儲器塊,緩沖區具有一定的固定大小,也稱為“容量”。 只能將 capacity 多的 byte,long,char 等寫入緩沖區。 緩沖區已滿后,需要清空它(讀取數據或清除它),然后才能將更多數據寫入。
position
將數據寫入緩沖區時,可以在某個位置執行操作。 position 初始值為 0 ,當一個 byte,long,char 等已寫入緩沖區時,position 被移動,指向緩沖區中的下一個單元以插入數據。 position 最大值為 capacity -1
從緩沖區讀取數據時,也可以從給定位置開始讀取數據。 當緩沖區從寫入模式切換到讀取模式時,position 將重置為 0 。當從緩沖區讀取數據時,將從 position 位置開始讀取數據,讀取后會將 position 移動到下一個要讀取的位置。
limit
在寫入模式下,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:
- 將數據從通道寫入緩沖區
- 通過緩沖區的 put() 方法,自己將數據寫入緩沖區。
這是一個示例,顯示了 Channel 如何將數據寫入 Buffer:
int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩沖區,返回讀入到緩沖區的字節數
buffer.put(127); // 此處的 127 是 byte 類型
put() 方法有許多其他版本,允許以多種不同方式將數據寫入 Buffer 。 例如,在特定位置寫入,或將一個字節數組寫入緩沖區。
flip() 切換緩沖區的讀寫模式
flip() 方法將 Buffer 從寫入模式切換到讀取模式。 調用 flip() 會將 position 設置回 0,並將 limit 的值設置為切換之前的 position 值。換句話說,limit 表示之前寫進了多少個 byte、char 等 —— 現在能讀取多少個 byte、char 等。
從緩沖區讀取數據
有兩種方法可以從 Buffer 中讀取數據:
- 將數據從緩沖區讀入通道。
- 使用 get() 方法之一,自己從緩沖區讀取數據。
以下是將緩沖區中的數據讀入通道的示例:
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 數組包裝到緩沖區中。 |
rewind() 倒帶
Buffer對象的 rewind() 方法將 position 設置回 0,因此可以重讀緩沖區中的所有數據, limit 則保持不變。
clear() 和 compact()
如果調用 clear() ,則將 position 設置回 0 ,並將 limit 被設置成 capacity 的值。換句話說,Buffer 被清空了。 但是 Buffer 中的實際存放的數據並未清除。
如果在調用 clear() 時緩沖區中有任何未讀數據,數據將被“遺忘”,這意味着不再有任何標記告訴讀取了哪些數據,還沒有讀取哪些數據。
如果緩沖區中仍有未讀數據,並且想稍后讀取它,但需要先寫入一些數據,這時候應該調用 compact() ,它會將所有未讀數據復制到 Buffer 的開頭,然后它將 position 設置在最后一個未讀元素之后。 limit 屬性仍設置為 capacity ,就像 clear() 一樣。 現在緩沖區已准備好寫入,並且不會覆蓋未讀數據。
mark() 和 reset()
以通過調用 Buffer 對象的 mark() 方法在 Buffer 中標記給定位置。 然后,可以通過調用 Buffer.reset() 方法將位置重置回標記位置,就像在標准 IO 中一樣。
buffer.mark();
// 調用 buffer.get() 等方法讀取數據...
buffer.reset(); // 設置 position 回到 mark 位置。
equals() 和 compareTo()
可以使用 equals() 和 compareTo() 比較兩個緩沖區。
equals() 成立的條件:
- 它們的類型相同(byte,char,int等)
- 它們在緩沖區中具有相同數量的剩余字節,字符等。
- 所有剩余的字節,字符等都相等。
如上,equals 僅比較緩沖區的一部分,而不是它內部的每個元素。 實際上,它只是比較緩沖區中的其余元素。
compareTo() 方法比較兩個緩沖區的剩余元素(字節,字符等), 在下列情況下,一個 Buffer 被視為“小於”另一個 Buffer:
- 第一個不相等的元素小於另一個 Buffer 中對應的元素 。
- 所有元素都相等,但第一個 Buffer 在第二個 Buffer 之前耗盡了元素(第一個 Buffer 元素較少)。