【java IO】|java NIO总结

发布时间:2023年12月20日

在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。

为了解决这个问题,在 Java1.4 版本引入了一种新的 I/O 模型(参考:浅谈IO模型)?—?NIO?(New IO,也称为 Non-blocking IO) 。NIO 弥补了同步阻塞 I/O 的不足,它在标准 Java 代码中提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个连接,大大提高了 I/O 效率和并发。

NIO核心组成

  • Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
  • Channel(通道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。
  • Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。

关系如图所示:

Buffer(缓冲区)

在?NIO 库中,所有数据都是用缓冲区处理的,这是新库和之前的 BIO 的一个重要区别,有点类似于 BIO 中的缓冲流。NIO 在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。 使用 NIO 在读写数据时,都是通过缓冲区进行操作。Buffer?的子类如下图所示。其中,最常用的是?ByteBuffer,它可以用来存储和操作字节数据。

public abstract class Buffer {
    // Invariants: mark <= position <= limit <= capacity

    // Buffer允许将位置直接定位到该标记处,这是一个可选属性;
    private int mark = -1;

    // 下一个可以被读写的数据的位置(索引)。从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。
    private int position = 0;

    // Buffer 中可以读/写数据的边界。写模式下,limit 代表最多能写入的数据,一般等于 capacity(可以通过limit(int newLimit)方法设置);读模式下,limit 等于 Buffer 中实际写入的数据大小。
    private int limit;

    // Buffer可以存储的最大数据量,Buffer创建时设置且不可改变;
    private int capacity;
}

Buffer 有读模式和写模式这两种模式,分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。Buffer 被创建之后默认是写模式,调用?flip()?可以切换到读模式。如果要再次切换回写模式,可以调用?clear()?或者?compact()?方法。

Buffer 常用方法:

  • get?: 读取缓冲区的数据
  • put?:向缓冲区写入数据
  • flip?:将缓冲区从写模式切换到读模式,它会将?limit?的值设置为当前?position?的值,将?position?的值设置为 0。
  • clear: 清空缓冲区,将缓冲区从读模式切换到写模式,并将?position?的值设置为 0,将?limit?的值设置为?capacity?的值。
Channel(通道)

Channel 是一个通道,它建立了与数据源(如文件、网络套接字等)之间的连接。我们可以利用它来读取和写入数据,就像打开了一条自来水管,让数据在 Channel 中自由流动。

BIO 中的流是单向的,分为各种?InputStream(输入流)和?OutputStream(输出流),数据只是在一个方向上传输。通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写。

Channel 与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。

?Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

Channel?的子类如图所示。

常用的是以下几种类型的通道:

  • FileChannel:文件访问通道;
  • SocketChannelServerSocketChannel:TCP 通信通道;
  • DatagramChannel:UDP 通信通道;

Channel 核心的两个方法:

  • read?:读取数据并写入到 Buffer 中。
  • write?:将 Buffer 中的数据写入到 Channel 中。

FileChannel读取文件数据:

RandomAccessFile reader = new RandomAccessFile("/Users/name/Documents/test.txt", "r"))
FileChannel channel = reader.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
Selector(选择器)

Selector(选择器) 是 NIO 中的一个关键组件,它允许一个线程处理多个 Channel。Selector 是基于事件驱动的 I/O 多路复用模型,主要运作原理是:通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行响应的 I/O 操作。

一个多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK 使用了?epoll()?代替传统的?select?实现,所以它并没有最大连接句柄?1024/2048?的限制。这也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。

Selector 可以监听以下四种事件类型:

  • SelectionKey.OP_ACCEPT:表示通道接受连接的事件,这通常用于?ServerSocketChannel
  • SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于?SocketChannel
  • SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读。
  • SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。

Selector是抽象类,可以通过调用此类的?open()?静态方法来创建 Selector 实例。Selector 可以同时监控多个?SelectableChannel?的?IO?状况,是非阻塞?IO?的核心。

一个 Selector 实例有三个?SelectionKey?集合:

  • 所有的?SelectionKey?集合:代表了注册在该 Selector 上的?Channel,这个集合可以通过?keys()?方法返回。
  • 被选择的?SelectionKey?集合:代表了所有可通过?select()?方法获取的、需要进行?IO?处理的 Channel,这个集合可以通过?selectedKeys()?返回。
  • 被取消的?SelectionKey?集合:代表了所有被取消注册关系的?Channel,在下一次执行?select()?方法时,这些?Channel?对应的?SelectionKey?会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。
文章来源:https://blog.csdn.net/u012203062/article/details/135091888
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。