1. NIO概述
1.1 什么是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中的缓冲区是双向可复用的。
1.2 IO和NIO区别
IO | NIO |
---|---|
面向流 | 面向缓冲区 |
阻塞IO | 非阻塞IO |
无 | 选择器 |
2. Buffer的数据存取
一个用于特定基本数据类行的容器。有java.nio包定义的,所有缓冲区都是抽象类Buffer的子类。
Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入到缓冲区,从缓冲区写入通道中的。
Buffer就像一个数组,可以保存多个相同类型的数据。根据类型不同(boolean除外),有以下Buffer常用子类:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
2.1 Buffer的概述
- 容量(capacity):表示Buffer最大数据容量,缓冲区容量不能为负,并且建立后不能修改。
- 限制(limit):第一个不应该读取或者写入的数据的索引,即位于limit后的数据不可以读写。缓冲区的限制不能为负,并且不能大于其容量(capacity)。
- 位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制(limit)。
- 标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
/**
* @Auther: 洺润Star
* @Date: 2020/3/9 11:41
* @Description:NIO
* 一:(缓冲区)buffer 用于NIO存储数据 支持多种不同的数据类型
* 1.byteBuffer
* 2.charBuffer
* 3.shortBuffer
* 4.IntBuffer
* 5.LongBuffer
* 6.FloatBuffer
* 7.DubooBuffer
*
* 二、缓冲区核心的方法
* put 存入数据到缓冲区
* get 获取缓冲区数据
* flip 开启读模式
*
* 三、缓冲区核心属性
* capacity:缓冲区最大容量,一旦声明不能改变。
* limit:界限(缓冲区可以操作的数据大小) limit后面的数据不能读写。
* position:缓冲区正在操作的位置
*/
public class Test01NIO {
public static void main(String[] args) {
System.out.println("-------------指定缓冲区大小---------------");
//1. 指定缓冲区大小
ByteBuffer allocate = ByteBuffer.allocate(1024);
System.out.println(allocate.position());//0
System.out.println(allocate.limit());//1024
System.out.println(allocate.capacity());//1024
//2. 向缓冲区存放五个数据
allocate.put("abcd1".getBytes());
System.out.println("-------------向缓冲区存放5个数据---------------");
System.out.println(allocate.position());//5
System.out.println(allocate.limit());//1024
System.out.println(allocate.capacity());//1024
//3. 开启读模式
allocate.flip();
System.out.println("-------------开启读模式---------------");
System.out.println(allocate.position());//0
System.out.println(allocate.limit());//5
System.out.println(allocate.capacity());//1024
byte[] bytes = new byte[allocate.limit()];
allocate.get(bytes);
System.out.println(new String(bytes,0,bytes.length));//abcd1
//4. 开启重复度模式
allocate.rewind();
System.out.println("-------------开启重复度读模式---------------");
System.out.println(allocate.position());//0
System.out.println(allocate.limit());//5
System.out.println(allocate.capacity());//1024
byte[] bytes2 = new byte[allocate.limit()];
allocate.get(bytes2);
System.out.println(new String(bytes2,0,bytes2.length));//abcd1
// 5.clean 清空缓冲区 数据依然存在,只不过数据被遗忘
System.out.println("-------------清空缓冲区---------------");
allocate.clear();
System.out.println(allocate.position());//0
System.out.println(allocate.limit());//1024
System.out.println(allocate.capacity());//1024
System.out.println((char)allocate.get());//a
}
}
输出结果:
注意:
- flip()和 rewind()的区别在于前者会直接从位置0开始读取,后者则会从上次读取的位置开始读取;
- 可以看到开启读模式前可用大小limit为1024,开启读模式后就变成了5(存放了字节的大小),而如果不开启读模式进行读取就会发生读取错误,如下:
2.2 mark与reset用法
标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法回到这个position。
/**
* @Auther: 洺润Star
* @Date: 2020/3/9 14:51
* @Description:mark 和reset
*/
public class Test02NIO {
public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
String str = "abcd1";
byteBuffer.put(str.getBytes());
//开启读模式
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes,0,2);
byteBuffer.mark();//标记位置
System.out.println(new String(bytes, 0, 2));//ab
System.out.println(byteBuffer.position());//2
byteBuffer.get(bytes, 2, 2);
System.out.println(new String(bytes, 2, 2));//cd
System.out.println(byteBuffer.position());//4
byteBuffer.reset();
System.out.println("使用reset重置恢复到mark位置..");
System.out.println(byteBuffer.position());//2
}
}
3. 直接缓冲区与非直接缓冲区别
非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中,程序要执行写操作需要将JVM中缓存区拷贝到物理内存中,然后再写到物理磁盘中,反过来就是读操作,由于存在一次拷贝过程所以效率较慢
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中,程序和物理磁盘间做读写操作的时候将不会走物理内存和JVM内存,而是直接从物理内存映射文件进行读取,效率更高。直接缓冲区于非直接相比会更加占用内存空间,而且安全性不如后者。
注:IO使用的缓存区为非直接缓冲区。
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。
3.1 通道(Channel)的原理获取
通道表示打开到 IO 设备(例如:文件、套接字)的连接。
若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
Channel 负责传输, Buffer 负责存储。通道是由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。
java.nio.channels.Channel 接口:
|--FileChannel
|--SocketChannel
|--ServerSocketChannel
|--DatagramChannel
获取通道
-
Java 针对支持通道的类提供了 getChannel() 方法
本地 IO:
FileInputStream/FileOutputStream
RandomAccessFile网络IO:
Socket
ServerSocket
DatagramSocket -
在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
-
在 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();
}
3.2 直接缓冲区与非直接缓冲耗时计算
/**
* @Auther: 洺润Star
* @Date: 2020/3/9 16:01
* @Description:通道的使用, 并测试直接和非直接缓冲区的速度差别
*/
public class Test03Channel {
// 1.利用通道完成文件复制(非直接缓冲区)
@Test
public void m1() throws IOException {
long startTime = System.currentTimeMillis();
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Administrator\\Desktop\\test.mp4");
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\test2.mp4");
//获取通道
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileChannel OutputStreamChannel = fileOutputStream.getChannel();
//指定缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (inputStreamChannel.read(byteBuffer)!=-1){
byteBuffer.flip();//切换读取模式
OutputStreamChannel.write(byteBuffer);
byteBuffer.clear();//清空缓冲区
}
//关闭连接
OutputStreamChannel.close();
inputStreamChannel.close();
fileOutputStream.close();
fileInputStream.close();
long endTime = System.currentTimeMillis();
System.out.println("共耗时:"+(endTime-startTime));//3276 3328 3323
}
//使用直接缓冲区完成文件的复制(内存映射文件)
@Test
public void m2() throws IOException {
long startTime = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("C:\\Users\\Administrator\\Desktop\\test.mp4"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("C:\\Users\\Administrator\\Desktop\\test.mp4"),
StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
// 映射文件
MappedByteBuffer inMapperBuff = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapperBuff = outChannel.map(FileChannel.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));//602 578 626
}
}
结果:
选取了大小为两百兆的视频文件,两种方式各测三次,结果为3276 3328 3323毫秒和602 578 626毫秒
3.3 分散读取与聚集写入
分散读取(scattering Reads):将通道中的数据分散到多个缓冲区中
聚集写入(gathering Writes):将多个缓冲区的数据聚集到通道中
/**
* @Auther: 洺润Star
* @Description:分散读取&聚集写入
*/
public class Test04ScatteringGathering {
public static void main(String[] args) throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("test.txt", "rw");
//1. 获取通道
FileChannel channel = raf1.getChannel();
// 2.分配指定大小的指定缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
ByteBuffer[] byteBuffers = {buf1,buf2};
channel.read(byteBuffers);
for (ByteBuffer byteBuffer : byteBuffers) {
// 切换为读取模式
byteBuffer.flip();
}
System.out.println(new String(byteBuffers[0].array(), 0, byteBuffers[0].limit()));
System.out.println("------------------分散读取--------------------");
System.out.println(new String(byteBuffers[1].array(), 0, byteBuffers[1].limit()));
// 聚集写入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(byteBuffers);
}
}
4. 字符集 Charset
编码:字符串->字节数组
解码:字节数组 -> 字符串
/**
* @Auther: 洺润Star
* @Date: 2020/3/9 17:32
* @Description:编码&解码
*/
public class Test05Encoder {
public static void main(String[] args) throws CharacterCodingException {
Charset cs1 = Charset.forName("GBK");
// 获取编码器
CharsetEncoder ce = cs1.newEncoder();
// 获取解码器
CharsetDecoder cd = cs1.newDecoder();
CharBuffer cBuf = CharBuffer.allocate(1024);
cBuf.put("今天明天后天!");
cBuf.flip();
// 编码
ByteBuffer bBuf = ce.encode(cBuf);
for (int i = 0; i < 12; i++) {
System.out.println(bBuf.get());
}
// 解码
bBuf.flip();
CharBuffer cBuf2 = cd.decode(bBuf);
System.out.println(cBuf2.toString());
System.out.println("-----------------尝试使用不同的解码器--------------------");
Charset cs2 = Charset.forName("GBK");
bBuf.flip();
CharBuffer cbeef = cs2.decode(bBuf);
System.out.println(cbeef.toString());
}
}