NIO :Non-blocking IO,在Java领域,也称为New I/O,是一种同步非阻塞的I/O模式,也是I/O多路复用的基础,已经被越来越多的应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。(本文参考过:Java 中 NIO 看这一篇就够了 - 知乎)
NIO与IO的区别
IO | NIO |
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
选择器(Selectors) |
NIO重要知识点
1.通道和缓冲区
Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer),通道表示程序到IO设备的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
2.面向流和面向缓冲区的解释
传统IO流
①我们需要把磁盘文件或者网络文件中的数据读取到程序中来,我们需要建立一个用于传输数据的管道,原来我们传输数据面对的直接就是管道里面一个个字节数据的流动(我们弄了一个 byte 数组,来回进行数据传递),所以说原来的 IO 它面对的就是管道里面的一个数据流动,所以我们说原来的 IO 是面向流的
②我们说传统的 IO 还有一个特点就是,它是单向的。解释一下就是:如果说我们想把目标地点的数据读取到程序中来,我们需要建立一个管道,这个管道我们称为输入流。相应的,如果如果我们程序中有数据想要写到目标地点去,我们也得再建立一个管道,这个管道我们称为输出流。所以我们说传统的 IO 流是单向的
NIO
①我们说只要是 IO ,那么就是为了完成数据传输的。
②即便你用 NIO ,它也是为了数据传输,所以你要想完成数据传输,你也得建立一个用于传输数据的通道,这个通道你不能把它理解为之前的水流了,但是你可以把它理解为铁路,铁路本身是不能完成运输的,铁路要想完成运输它必须依赖火车,说白了这个通道就是为了连接目标地点和源地点。所以注意通道本身不能传输数据,要想传输数据必须要有缓冲区,这个缓冲区你就可以完全把它理解为火车,比如说你现在想把程序中的数据写到文件中,那么你就可以把数据都写到缓冲区,然后缓冲区通过通道进行传输,最后再把数据从缓冲区拿出来写到文件中,你想把文件中的数据传数到程序中,也是一个道理,把数据写到缓冲区,缓冲区通过通道进行传输,到程序中把数据拿出来。所以我们说原来的 IO 单向的现在的缓冲区是双向的,这种传输数据的方式也叫面向缓冲区。总结一下,就是通道只负责连接,缓冲区才负责存储数据。
3、缓冲区的数据存取
缓冲区(Buffer):一个用于特定基本数据类型的容器。由java.nio包定义,所有缓冲区都是Buffer抽象类的子类 。
1、缓冲区的类型
缓冲区(Buffer):在Java NIO中负责数据的存取。缓冲区就是数组。用于存储不同类型的数据。根据数据类型的不同(boolean除外),提供了相应类型的缓冲区:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
上述缓冲区管理方式几乎一致,都是通过allocate()来获取缓冲区
2、缓冲区存取数据的两个核心方法
put():存入数据到缓冲区中,get():获取缓冲区中的数据。
3、缓冲区中的四个核心属性
capacity
: 容量,表示缓冲区中最大存储数据的容量。一旦声明不能更改。limit
: 界限,表示缓冲区中可以操作数据的大小。(limit 后的数据不能进行读写)position
: 位置,表示缓冲区中正在操作数据的位置。mark
: 标记,表示记录当前 position 的位置。可以通过reset()
恢复到mark
的位置。
public class TestBuffer {
public static void test1() {
String str = "abcde";
//分配一个指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //1024
System.out.println(byteBuffer.position()); //0
//利用 put() 存入数据到缓冲区中
byteBuffer.put(str.getBytes());
System.out.println("---------put-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //1024
System.out.println(byteBuffer.position()); //5
//切换到读数据模式
byteBuffer.flip();
System.out.println("---------flip-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //5,limit 表示可以操作数据的大小,只有5 个字节的数据给你读,所以可操作数据大小是5
System.out.println(byteBuffer.position()); //0,读数据要从第 0 个位置开始读
//利用 get() 读取缓冲区中的数据
byte[] dst = new byte[byteBuffer.limit()];
byteBuffer.get(dst);
System.out.println(new String(dst,0,dst.length));
System.out.println("---------get-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //5,可以读取数据的大小依然是 5 个
System.out.println(byteBuffer.position()); //5,读完之后位置变到了第 5 个
//rewind() 可重复读
byteBuffer.rewind(); //这个方法调用完后,又变成了读模式
System.out.println("---------rewind-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //5
System.out.println(byteBuffer.position()); //0
//clear() 清空缓冲区,虽然缓冲区被清空了,但是缓冲区中的数据依然存在,只是出于"被遗忘"状态。意思其实是,缓冲区中的界限、位置等信息都被置为最初的状态了,所以你无法再根据这些信息找到原来的数据了,原来数据就出于"被遗忘"状态
byteBuffer.clear();
System.out.println("---------clear-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //1024
System.out.println(byteBuffer.position()); //0
}
4、直接缓冲区和非直接缓冲区
1)、非直接缓冲区
通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存之中。
应用程序和磁盘之间想要传输数据,是没有办法直接进行传输的。操作系统出于安全的考虑,会经过上图几个步骤。例如,我应用程序想从磁盘中读取一个数据,这时候我应用程序向操作系统发起一个读请求,那么首先磁盘中的数据会被读取到内核地址空间中,然后会把内核地址空间中的数据拷贝到用户地址空间中(其实就是 JVM 内存中),最后再把这个数据读取到应用程序中来。
同样,如果我应用程序有数据想要写到磁盘中去,那么它会首先把这个数据写入到用户地址空间中去,然后把数据拷贝到内核地址空间,最后再把这个数据写入到磁盘中去。
2)、直接缓冲区
通过 allocateDirect()
方法分配缓冲区,将缓冲区建立在物理内存之中。直接用物理内存作为缓冲区,读写数据直接通过物理内存进行。
public static void test3() {
// 分配直接缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
// 判断是直接缓冲区还是非直接缓冲区
System.out.println(byteBuffer.isDirect());
}
5、通道
- 通道(channel):由
java.nio.channels
包定义的。Channel
表示IO
源与目标打开的连接。Channel
类似于传统的流,只不过Channel
本身不能直接访问数据,Channel
只能与Buffer
进行交互。 - 通道用于源节点与目标节点的连接。在
Java NIO
中负责缓冲区中数据的传输。Channel
本身不存储数据,因此需要配合缓冲区进行传输。
主要实现类 :
java.nio.channels.Channel
包下:
FileChannel
SocketChannel
ServerSocketChannel
DatagramChannel
获取通道:
1、Java 针对支持通道的类提供了 getChannel() 方法
本地 IO:
FileInputStream/FileOutputStream
RandomAccessFile
网络 IO:
Socket
ServerSocket
DatagramSocket
以上几个类都可以通过调用 getChannel()
方法获取通道
2、在 JDK1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
3、在 JDK1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()
方法
6、通道数据传输和内存映射文件
案例一 使用通道完成文件的赋值(非直接缓冲区)
public static void test1() throws Exception {
// 利用通道完成文件的复制(非直接缓冲区)
FileInputStream fis = new FileInputStream("a.txt");
FileOutputStream fos = new FileOutputStream("b.txt");
// 获取通道
FileChannel fisChannel = fis.getChannel();
FileChannel foschannel = fos.getChannel();
// 通道没有办法传输数据,必须依赖缓冲区
// 分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将通道中的数据存入缓冲区中
while (fisChannel.read(byteBuffer) != -1) { // fisChannel 中的数据读到 byteBuffer 缓冲区中
byteBuffer.flip(); // 切换成读数据模式
// 将缓冲区中的数据写入通道
foschannel.write(byteBuffer);
byteBuffer.clear(); // 清空缓冲区
}
foschannel.close();
fisChannel.close();
fos.close();
fis.close();
}
案例二、使用直接缓冲区完成文件的赋值(内存映射文件)
public static void test2() throws Exception {
// 使用直接缓冲区完成文件的复制(内存映射文件)
/**
* 使用 open 方法来获取通道
* 需要两个参数
* 参数1:Path 是 JDK1.7 以后给我们提供的一个类,代表文件路径
* 参数2:Option 就是针对这个文件想要做什么样的操作
* --StandardOpenOption.READ :读模式
* --StandardOpenOption.WRITE :写模式
* --StandardOpenOption.CREATE :如果文件不存在就创建,存在就覆盖
*/
FileChannel inChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("c.txt"), StandardOpenOption.WRITE,
StandardOpenOption.READ, StandardOpenOption.CREATE);
/**
* 内存映射文件
* 这种方式缓冲区是直接建立在物理内存之上的
* 所以我们就不需要通道了
*/
MappedByteBuffer inMapped = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapped = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
// 直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMapped.limit()];
inMapped.get(dst); // 把数据读取到 dst 这个字节数组中去
outMapped.put(dst); // 把字节数组中的数据写出去
inChannel.close();
outChannel.close();
}
public static void test3() throws Exception {
/**
* 通道之间的数据传输(直接缓冲区的方式)
* transferFrom
* transferTo
*/
FileChannel inChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
inChannel.transferTo(0, inChannel.size(), outChannel);
// 或者可以使用下面这种方式
//outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
}
7、分散读取与聚集写入
- 分散读取(Scattering Reads)是指从 Channel 中读取的数据 "分散" 到多个 Buffer 中
- 聚集写入(Gathering Writes)是指将多个 Buffer 中的数据 "聚集" 到 Channel
public static void test4() throws Exception { RandomAccessFile raf = new RandomAccessFile("a.txt", "rw"); // 获取通道 FileChannel channel = raf.getChannel(); // 分配指定大小缓冲区 ByteBuffer buf1 = ByteBuffer.allocate(2); ByteBuffer buf2 = ByteBuffer.allocate(1024); // 分散读取 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())); // 打印 he System.out.println(new String(bufs[1].array(), 0, bufs[1].limit())); // 打印 llo // 聚集写入 RandomAccessFile raf2 = new RandomAccessFile("e.txt","rw"); // 获取通道 FileChannel channel2 = raf2.getChannel(); channel2.write(bufs); // 把 bufs 里面的几个缓冲区聚集到 channel2 这个通道中,聚集到通道中,也就是到了 e.txt 文件中 channel2.close(); }