NIO基礎(第九天)
什麼是NIO
Java NIO(New IO)是一個可以替代標準Java IO API的IO API(從Java 1.4開始),Java NIO提供了與標準IO不同的IO工作方式。
Java NIO: Channels and Buffers(通道和緩衝區)
標準的IO基於位元組流和字元流進行操作的,而NIO是基於通道(Channel)和緩衝區(Buffer)進行操作,資料總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。
Java NIO: Non-blocking IO(非阻塞IO)
Java NIO可以讓你非阻塞的使用IO,例如:當執行緒從通道讀取資料到緩衝區時,執行緒還是可以進行其他事情。當資料被寫入到緩衝區時,執行緒可以繼續處理它。從緩衝區寫入通道也類似。
Java NIO: Selectors(選擇器)
Java NIO引入了選擇器的概念,選擇器用於監聽多個通道的事件(比如:連線開啟,資料到達)。因此,單個的執行緒可以監聽多個數據通道。
注意:傳統IO是單向,NIO是雙向的。
Buffer的資料存取
一個用於特定基本資料類行的容器。由java.nio包定義的,所有緩衝區都是抽象類Buffer的子類。
Java NIO中的Buffer主要用於與NIO通道進行互動,資料是從通道讀入到緩衝區,從緩衝區寫入通道中的。Buffer就像一個數組,可以儲存多個相同型別的資料。根據型別不同(boolean除外),有以下Buffer常用子類:
ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
Buffer的概述
-
容量(capacity):表示Buffer最大資料容量,緩衝區容量不能為負,並且建立後不能修改。
-
限制(limit):第一個不應該讀取或者寫入的資料的索引,即位於limit後的資料不可以讀寫。緩衝區的限制不能為負,並且不能大於其容量(capacity)。
-
位置(position):下一個要讀取或寫入的資料的索引。緩衝區的位置不能為負,並且不能大於其限制(limit)。
-
標記(mark)與重置(reset):標記是一個索引,通過Buffer中的mark()方法指定Buffer中一個特定的position,之後可以通過呼叫reset()方法恢復到這個position。
/**
* (緩衝區)buffer 用於NIO儲存資料 支援多種不同的資料型別
* 1.byteBuffer
* 2.charBuffer
* 3.shortBuffer
* 4.IntBuffer
* 5.LongBuffer
* 6.FloatBuffer
* 7.DubooBuffer
* 上述緩衝區管理的方式 幾乎
* 通過allocate() 獲取緩衝區
* 二、緩衝區核心的方法 put 存入資料到緩衝區 get 獲取緩衝區資料 flip 開啟讀模式
* 三、緩衝區四個核心屬性
* capacity:緩衝區最大容量,一旦宣告不能改變。 limit:介面(緩衝區可以操作的資料大小) limit後面的資料不能讀寫。
* position:緩衝區正在操作的位置
*/
public class TestNIO {
public static void main(String[] args) {
// 1.指定緩衝區大小1024
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("--------------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// 2.向緩衝區存放5個數據
buf.put("abcd1".getBytes());
System.out.println("--------------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// 3.開啟讀模式
buf.flip();
System.out.println("----------開啟讀模式...----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
byte[] bytes = new byte[buf.limit()];
buf.get(bytes);
System.out.println(new String(bytes, 0, bytes.length));
System.out.println("----------重複讀模式...----------");
// 4.開啟重複讀模式
buf.rewind();
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
byte[] bytes2 = new byte[buf.limit()];
buf.get(bytes2);
System.out.println(new String(bytes2, 0, bytes2.length));
// 5.clean 清空緩衝區 資料依然存在,只不過資料被遺忘
System.out.println("----------清空緩衝區...----------");
buf.clear();
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
System.out.println((char)buf.get());
}
}
make與rest用法
標記(mark)與重置(reset):標記是一個索引,通過Buffer中的mark()方法指定Buffer中一個特定的position,之後可以通過呼叫reset()方法恢復到這個position。
public class Test002 {
public static void main(String[] args) {
ByteBuffer buf = ByteBuffer.allocate(1024);
String str = "abcd1";
buf.put(str.getBytes());
// 開啟讀取模式
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst, 0, 2);
buf.mark();
System.out.println(new String(dst, 0, 2));
System.out.println(buf.position());
buf.get(dst, 2, 2);
System.out.println(new String(dst, 2, 2));
System.out.println(buf.position());
buf.reset();
System.out.println("重置恢復到mark位置..");
System.out.println(buf.position());
}
}
直接緩衝區與非直接緩衝區別
非直接緩衝區:通過 allocate() 方法分配緩衝區,將緩衝區建立在 JVM 的記憶體中。
直接緩衝區:通過 allocateDirect() 方法分配直接緩衝區,將緩衝區建立在實體記憶體中。可以提高效率。
-
位元組緩衝區要麼是直接的,要麼是非直接的。如果為直接位元組緩衝區,則 Java虛擬機器會盡最大努力直接在此緩衝區上執行本機 I/O 操作。也就是說,在每次呼叫基礎作業系統的一個本機 I/O 操作之前(或之後),虛擬機器都會盡量避免將緩衝區的內容複製到中間緩衝區中(或從中間緩衝區中複製內容)。
-
直接位元組緩衝區可以通過呼叫此類的 allocateDirect() 工廠方法來建立。此方法返回的緩衝區進行分配和取消分配所需成本通常高於非直接緩衝區。直接緩衝區的內容可以駐留在常規的垃圾回收堆之外,因此,它們對應用程式的記憶體需求量造成的影響可能並不明顯。所以,建議將直接緩衝區主要分配給那些易受基礎系統的本機 I/O 操作影響的大型、持久的緩衝區。一般情況下,最好僅在直接緩衝區能在程式效能方面帶來明顯好處時分配它們。
-
直接位元組緩衝區還可以通過 FileChannel 的 map() 方法 將檔案區域直接對映到記憶體中來建立。該方法返回MappedByteBuffer。 Java 平臺的實現有助於通過 JNI 從本機程式碼建立直接位元組緩衝區。如果以上這些緩衝區中的某個緩衝區例項指的是不可訪問的記憶體區域,則試圖訪問該區域不會更改該緩衝區的內容,並且將會在訪問期間或稍後的某個時間導致丟擲不確定的異常。
// 使用直接緩衝區完成檔案的複製(記憶體對映檔案)
public void test2() throws IOException {
long start = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("f://1.mp4"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("f://2.mp4"), StandardOpenOption.WRITE,
StandardOpenOption.READ, StandardOpenOption.CREATE);
// 記憶體對映檔案
MappedByteBuffer inMappedByteBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedByteBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
// 直接對緩衝區進行資料的讀寫操作
byte[] dsf = new byte[inMappedByteBuf.limit()];
inMappedByteBuf.get(dsf);
outMappedByteBuffer.put(dsf);
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
// 1.利用通道完成檔案的複製(非直接緩衝區)
public void test1() throws IOException { // 4400
long start = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("f://1.mp4");
FileOutputStream fos = new FileOutputStream("f://2.mp4");
// ①獲取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
// ②分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
while (inChannel.read(buf) != -1) {
buf.flip();// 切換為讀取資料
// ③將緩衝區中的資料寫入通道中
outChannel.write(buf);
buf.clear();
}
outChannel.close();
inChannel.close();
fos.close();
fis.close();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
通道(Channel)的原理獲取
通道表示開啟到 IO 裝置(例如:檔案、套接字)的連線。若需要使用 NIO 系統,需要獲取用於連線 IO 裝置的通道以及用於容納資料的緩衝區。然後操作緩衝區,對資料進行處理。Channel 負責傳輸, Buffer 負責儲存。通道是由 java.nio.channels 包定義的。 Channel 表示 IO 源與目標開啟的連線。Channel 類似於傳統的“流”。只不過 Channel本身不能直接訪問資料, Channel 只能與Buffer 進行互動。
java.nio.channels.Channel 介面:
|--FileChannel
|--SocketChannel
|--ServerSocketChannel
|--DatagramChannel
獲取通道
1. Java 針對支援通道的類提供了 getChannel() 方法
本地 IO:
FileInputStream/FileOutputStream
RandomAccessFile
網路IO:
Socket
ServerSocket
DatagramSocket
2. 在 JDK 1.7 中的 NIO.2 針對各個通道提供了靜態方法 open()
3. 在 JDK 1.7 中的 NIO.2 的 Files 工具類的 newByteChannel()
@Test
// 使用直接緩衝區完成檔案的複製(記憶體對映檔案)
public void test2() throws IOException {
FileChannel inChannel = FileChannel.open(Paths.get("1.png"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
// 對映檔案
MappedByteBuffer inMapperBuff = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapperBuff = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
// 直接對緩衝區進行資料讀寫操作
byte[] dst = new byte[inMapperBuff.limit()];
inMapperBuff.get(dst);
outMapperBuff.put(dst);
outChannel.close();
inChannel.close();
}
@Test
// 1.利用通道完成檔案複製(非直接緩衝區)
public void test1() throws IOException {
FileInputStream fis = new FileInputStream("1.png");
FileOutputStream fos = new FileOutputStream("2.png");
// ①獲取到通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
// ②分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
while (inChannel.read(buf) != -1) {
buf.flip();// 切換到讀取模式
outChannel.write(buf);
buf.clear();// 清空緩衝區
}
// 關閉連線
outChannel.close();
inChannel.close();
fos.close();
fis.close();
}
直接緩衝區與非直接緩衝耗時計算
@Test
// 使用直接緩衝區完成檔案的複製(記憶體對映檔案) //428、357
public void test2() throws IOException {
long startTime = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("f://1.mp4"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("f://2.mp4"), StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
// 對映檔案
MappedByteBuffer inMapperBuff = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapperBuff = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
// 直接對緩衝區進行資料讀寫操作
byte[] dst = new byte[inMapperBuff.limit()];
inMapperBuff.get(dst);
outMapperBuff.put(dst);
outChannel.close();
inChannel.close();
long endTime = System.currentTimeMillis();
System.out.println("記憶體對映檔案耗時:"+(endTime-startTime));
}
@Test
// 1.利用通道完成檔案複製(非直接緩衝區)
public void test1() throws IOException { //11953 、3207、3337
long startTime = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("f://1.mp4");
FileOutputStream fos = new FileOutputStream("f://2.mp4");
// ①獲取到通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
// ②分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
while (inChannel.read(buf) != -1) {
buf.flip();// 切換到讀取模式
outChannel.write(buf);
buf.clear();// 清空緩衝區
}
// 關閉連線
outChannel.close();
inChannel.close();
fos.close();
fis.close();
long endTime = System.currentTimeMillis();
System.out.println("非緩衝區:"+(endTime-startTime));
}
分散讀取與聚集寫入
分散讀取(scattering Reads):將通道中的資料分散到多個緩衝區中。
聚集寫入(gathering Writes):將多個緩衝區的資料聚集到通道中。
RandomAccessFile raf1 = new RandomAccessFile("test.txt", "rw");
// 1.獲取通道
FileChannel channel = raf1.getChannel();
// 2.分配指定大小的指定緩衝區
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
// 3.分散讀取
ByteBuffer[] bufs = { buf1, buf2 };
channel.read(bufs);
for (ByteBuffer byteBuffer : bufs) {
// 切換為讀取模式
byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
System.out.println("------------------分算讀取線分割--------------------");
System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
// 聚集寫入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);