Java NIO - tenji/ks GitHub Wiki
Non-blocking I/O (Java)
java.nio(NIO 代表新输入/输出)是 Java 编程语言 API 的集合,提供密集型 I/O 操作的功能。它是由 Sun Microsystems 在 Java 的 J2SE 1.4 版本中引入的,以补充现有的标准 I/O。NIO 的 API 旨在提供对现代操作系统的低级 I/O 操作的访问。
一、核心组件
1.1 Buffers(缓存)
NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
继承关系图:
1.2 Channels(通道)
Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。
继承关系图:
1.3 Selectors(选择器)
Java NIO 选择器是一个组件,可以检查一个或多个 Java NIO 通道实例,并确定哪些通道已准备好用于例如读或者写。这样,单个线程可以管理多个通道,从而管理多个网络连接。
SelectionKey
表示 SelectableChannel 在 Selector 中的注册的标记。每次向选择器注册通道时就会创建一个 SelectionKey。通过调用某个 Key 的 cancel 方法、关闭其通道,或者通过关闭其选择器来取消该 Key 之前,它一直保持有效。
二、NIO vs BIO
2.1 Java NIO 和 IO 之间的主要区别
面向流(Stream Oriented)与面向缓冲区(Buffer Oriented)
Java IO 是面向流的,这意味着你一次从流中读取一个或多个字节。如何处理读取的字节取决于你。它们没有缓存在任何地方。此外,你无法在流中的数据中来回移动(因为 Stream 无法重复读取)。如果需要前后移动从流中读取的数据,则需要首先将其缓存在缓冲区中。
Java NIO 的面向缓冲区的方法略有不同。数据被读入缓冲区并随后进行处理。你可以根据需要在缓冲区中前后移动。这为你在处理过程中提供了更多的灵活性。但是,你还需要检查缓冲区是否包含完全处理所需的所有数据。并且,你需要确保将更多数据读入缓冲区时,不会覆盖缓冲区中尚未处理的数据。
阻塞 IO 与非阻塞 IO
Java IO 的各种流都是阻塞的。这意味着,当线程调用 read()
或 write()
时,该线程将被阻塞,直到有一些数据可供读取或数据已完全写入。在此期间,线程不能执行任何其他操作。
Java NIO 的非阻塞模式使线程能够请求从通道读取数据,并且仅获取当前可用的数据,或者如果当前没有可用数据则根本不获取任何数据。线程可以继续执行其他操作,而不是在数据可供读取之前保持阻塞状态。
非阻塞写入也是如此。线程可以请求将一些数据写入通道,但不能等待数据完全写入。然后线程可以继续同时执行其他操作。
线程被阻塞时,CPU 也会被占用吗?
不是。当一个连接在处理 I/O 的时候,系统是阻塞的,但 CPU 是被释放出来的。
2.2 使用方式不同
在 BIO 设计中,你从 InputStream 或 Reader 中逐字节读取数据。想象一下你正在处理基于行的文本数据流。例如:
Name: Anna
Age: 25
Email: [email protected]
Phone: 1234567890
该文本行流可以这样处理:
InputStream input = ... ; // get the InputStream from the client socket
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();
请注意处理状态是如何根据程序执行的程度来确定的。换句话说,一旦第一个 reader.readLine() 方法返回,你就可以确定已经读取了一整行文本。readLine() 会阻塞,直到读取整行,这就是原因。你还知道该行包含名称。类似地,当第二个 readLine() 调用返回时,您知道该行包含年龄等。下图说明了这一过程:
NIO 实现看起来会有所不同,我们先将从通道读取数据到 Buffer,再从 Buffer 中读取数据。这是一个简化的示例:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
请注意第二行,它将字节从通道读取到 ByteBuffer 中。当该方法调用返回时,你不知道所需的所有数据是否都在缓冲区内。你只知道缓冲区包含一些字节。这使得处理变得有些困难。
想象一下,如果在第一次 read(buffer) 调用之后,读入缓冲区的所有内容都是半行。例如,“Name: An”。你能处理这些数据吗?不可以。你需要等待至少一整行数据进入缓冲区,然后才有意义处理任何数据。
那么,你如何知道缓冲区是否包含足够的数据来进行处理呢?好吧,你不知道。找出答案的唯一方法是查看缓冲区中的数据。结果是,你可能必须多次检查缓冲区中的数据才能知道所有数据是否都在其中。这不仅效率低下,而且在程序设计方面可能会变得混乱。例如:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
如果缓冲区已满,则可以进行处理。如果它未满,你可能能够部分处理其中的任何数据(如果这在你的特定情况下有意义)。在很多情况下,事实并非如此。
下图说明了 is-data-in-buffer-ready 循环:
2.3 适用场景不同
如果你需要同时管理数千个打开的连接,每个连接只发送一点数据,例如聊天服务器,那么在 NIO 中实现服务器可能是一个优势。同样,如果你需要与其他计算机保持大量开放连接,例如在 P2P 网络中,使用单个线程来管理所有出站连接可能是一个优势。这一单线程、多连接设计如下图所示:
如果你的连接较少且带宽非常高,一次发送大量数据,那么经典的 IO 服务器实现可能是最合适的。下图展示了经典的 IO 服务器设计:
NIO 性能一定由于 BIO 吗?
使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。