NIO 简介
在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。
为了解决这个问题,在 Java1.4 版本引入了一种新的 I/O 模型 — NIO (New IO,也称为 Non-blocking IO) 。NIO 弥补了同步阻塞 I/O 的不足,它在标准 Java 代码中提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个连接,大大提高了 I/O 效率和并发。
NIO 核心组件
NIO 主要包括以下三个核心组件:
Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
Channel(通道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。
Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。
三者的关系如下图所示(暂时不理解没关系,后文会详细介绍):
Buffer(缓冲区)
在传统的 BIO 中,数据的读写是面向流的, 分为字节流和字符流。
在 Java 1.4 的 NIO 库中,所有数据都是用缓冲区处理的,这是新库和之前的 BIO 的一个重要区别,有点类似于 BIO 中的缓冲流。NIO 在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。 使用 NIO 在读写数据时,都是通过缓冲区进行操作。
Buffer 的子类如下图所示。其中,最常用的是 ByteBuffer,它可以用来存储和操作字节数据。
Buffer 的子类
你可以将 Buffer 理解为一个数组,IntBuffer
、FloatBuffer
、CharBuffer
等分别对应 int[]
、float[]
、char[]
等。
为了更清晰地认识缓冲区,我们来简单看看Buffer
类中定义的四个成员变量:
public abstract class Buffer {// Invariants: mark <= position <= limit <= capacityprivate int mark = -1;private int position = 0;private int limit;private int capacity;
}
这四个成员变量的具体含义如下:
容量(capacity):Buffer可以存储的最大数据量,Buffer创建时设置且不可改变;
界限(limit):Buffer 中可以读/写数据的边界。写模式下,limit 代表最多能写入的数据,一般等于 capacity(可以通过limit(int newLimit)方法设置);读模式下,limit 等于 Buffer 中实际写入的数据大小。
位置(position):下一个可以被读写的数据的位置(索引)。从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。
标记(mark):Buffer允许将位置直接定位到该标记处,这是一个可选属性;
并且,上述变量满足如下的关系:0 <= mark <= position <= limit <= capacity 。
另外,Buffer 有读模式和写模式这两种模式,分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。Buffer 被创建之后默认是写模式,调用 flip() 可以切换到读模式。如果要再次切换回写模式,可以调用 clear() 或者 compact() 方法。
Buffer 对象不能通过 new 调用构造方法创建对象 ,只能通过静态方法实例化 Buffer。
这里以 ByteBuffer为例进行介绍:
Buffer 最核心的两个方法:
get : 读取缓冲区的数据
put :向缓冲区写入数据
除上述两个方法之外,其他的重要方法:
flip :将缓冲区从写模式切换到读模式,它会将 limit 的值设置为当前 position 的值,将 position 的值设置为 0。
clear: 清空缓冲区,将缓冲区从读模式切换到写模式,并将 position 的值设置为 0,将 limit 的值设置为 capacity 的值。
……
Buffer 中数据变化的过程
import java.nio.*;public class CharBufferDemo {public static void main(String[] args) {// 分配一个容量为8的CharBufferCharBuffer buffer = CharBuffer.allocate(8);System.out.println("初始状态:");printState(buffer);// 向buffer写入3个字符buffer.put('a').put('b').put('c');System.out.println("写入3个字符后的状态:");printState(buffer);// 调用flip()方法,准备读取buffer中的数据,将 position 置 0,limit 的置 3buffer.flip();System.out.println("调用flip()方法后的状态:");printState(buffer);// 读取字符while (buffer.hasRemaining()) {System.out.print(buffer.get());}// 调用clear()方法,清空缓冲区,将 position 的值置为 0,将 limit 的值置为 capacity 的值buffer.clear();System.out.println("调用clear()方法后的状态:");printState(buffer);}// 打印buffer的capacity、limit、position、mark的位置private static void printState(CharBuffer buffer) {System.out.print("capacity: " + buffer.capacity());System.out.print(", limit: " + buffer.limit());System.out.print(", position: " + buffer.position());System.out.print(", mark 开始读取的字符: " + buffer.mark());System.out.println("\n");}
}
输出:
初始状态:
capacity: 8, limit: 8, position: 0写入3个字符后的状态:
capacity: 8, limit: 8, position: 3准备读取buffer中的数据!调用flip()方法后的状态:
capacity: 8, limit: 3, position: 0读取到的数据:abc调用clear()方法后的状态:
capacity: 8, limit: 8, position: 0