NIO: Non-blocking IO
,是指 jdk1.4
及以上版本里提供的 New IO
。NIO
弥补了原来的 I/O
的不足,它在标准 Java
代码中提供了高速的、面向块的 I/O
,所有数据都是用缓冲区处理的。
基本概念
NIO
将最耗时的 I/O
操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。原来的 I/O
以流的方式处理数据,而 NIO
以块的方式处理数据。
流和块的区别
- 流
I/O
面向流的I/O
系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。但是面向流的I/O
通常相当慢。 - 块
I/O
面向块的I/O
系统以块的形式处理数据。每一个操作都在异步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的操作较复杂。
NIO
的核心对象
Channel
通道:是对原I/O
包中的流的模拟。Channel
是一个对象,所有数据必须通过它读取和写入。默认是阻塞模式。Buffer
缓冲区:实质上是一个容器对象,所有的NIO
数据都是通过它来处理的。发送给一个通道的所有对象都必须首先放到缓冲区中;同样从通道中读取的任何数据都要读到缓冲区中。
Channel
数据交互
永远不会将字节直接写入通道,或者直接从通道中读字节。通道中的数据总是要先读到一个 Buffer
,或者总是要从一个 Buffer
中写入。
通道类型
通道与流的不同之处在于通道是双向的,而流是单向的(一个流必须是 InputStream
或者 OutputStream
的子类)。通道可以用于读、写或者同时用于读写。
因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX
模型中,底层操作系统通道是双向的。
常用 Channel
FileChannel
从文件中读写数据,无法设置为非阻塞模式,它总是运行在阻塞模式下。DatagramChannel
通过UDP
读写网络中的数据。SocketChannel
通过TCP
读写网络中的数据。ServerSocketChannel
服务端,可以监听新进来的TCP
连接。
分散/聚集 Scatter/Gather I/O
分散/聚集 Scatter/Gather I/O
:用于描述从通道中读取或者写入到通道的操作,但是使用多个而不是单个缓冲区来保存数据的读写方法。
一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。分散/聚集 I/O
对于将数据流划分为单独的部分很有用,这有助于实现复杂的数据格式。
1 | public interface ScatteringByteChannel |
- 分散读
分散读取会自动找到有空间接受数据的第一个缓冲区,在这个缓冲区填满后,才能移动到下一个缓冲区。在实际运用中,适合消息长度固定的例子。如网络应用程序中,每一个消息被划分为固定长度的头部和固定长度的正文。可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。 - 聚集写
聚集写对于把一组单独的缓冲区中组成单个数据流写入同一个通道。将数据写入到通道中时,注意缓冲区只有position
和limit
之间的数据才会被写入,所以在实际运用中,可以写入长度不固定的消息。为了与上面的消息例子保持一致,可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流,以便跨越网络传输消息。
Buffer
缓冲区实质上是一个数组,通常它是一个字节数组,也可以使用其他类型的数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。本质上是读写数据的这块内存被包装成 NIO Buffer
对象,并提供了一组方法,用来方便的访问该块内存。最常用的缓冲区类型是 ByteBuffer
,可以在其底层字节数组上进行字节的获取和设置 get/set
。
每一种基本 Java
类型(除了 Boolean
)都有一种缓冲区类型:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
状态变量
状态变量是每一个读/写操作后,记录缓冲区的状态。通过记录和跟踪这些变化,缓冲区能够内部地管理自己的资源。缓冲区有两种模式:读模式和写模式。如下三个变量,记录了缓冲区在这两个模式下的状态:
capacity
可以储存在缓冲区中的最大数据容量。position
写模式:表示当前位置,初始化为 0,最大值为capacity - 1
。
读模式:从该位置开始读,在缓冲区从写模式切换到读模式时被重置为 0。limit
写模式:表示最多能往缓冲区写入多少数据,写模式下limit = capacity
。
读模式:表示最多能读到多少数据。写模式切换为读模式时,limit
被设置为写模式下的position
,而position
会被重置为 0,即能读到之前写入的所有数据。
其中:position <= limit <= capacity
。读写模式切换方法 flip()
的源码:
1 | public final Buffer flip() { |
缓冲区使用的基本步骤
- 写入数据到
Buffer
- 调用
flip()
方法,切换到读模式 - 从
Buffer
中读取数据 - 调用
clear()
方法或者compact()
方法,切换到写模式
1 | RandomAccessFile aFile = new RandomAccessFile("nio-data.txt", "rw"); |
常见 API
1 | // 缓冲区写模式切换到读模式 |
分片
片:是缓冲区的子缓冲区,根据现有的缓冲区,从当前位置创建一个子缓冲区,这个新创建的缓冲区与原来的缓冲区共享同一个底层数据数组。public abstract ByteBuffer slice();
读写示例
读和写是 I/O
的基本过程。从一个通道中读取很简单:只需创建一个缓冲区,然后让通道将数据读到这个缓冲区中;写入也相当简单:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入操作。在 NIO
系统中,任何时候执行一个读写操作,都是通道和缓冲区的读写交互。
读
- 从
FileInputStream
获取Channel
- 创建
Buffer
- 将数据从
Channel
读到Buffer
中
我们不需要告诉通道要读多少数据到缓冲区中,每一个缓冲区都有内部统计机制,它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据。
写
- 从
FileOutputStream
获取Channel
- 创建
Buffer
,并填充被写的数据 - 将数据从
Buffer
写入Channel
这里同样不需要告诉通道要写入多数据,缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。
示例
1 | FileInputStream fin = new FileInputStream( "readandshow.txt" ); |
FileChannel
FileChannel
是一个连接到文件的通道,可以通过文件通道读写文件。一般情况下,无法直接打开一个 FileChannel
,需要通过使用 InputStream, OutputStream, RandomAccessFile
来获取 FileChannel
实例。
1 | FileInputStream fin = new FileInputStream( "readandshow.txt" ); |
常见 API
1 | // 读写 |
通道之间数据传输
如果两个通道中有一个是 FileChannel
,那你可以直接将数据从一个通道中传输到另外一个通道。
1 | public abstract long transferTo(long position, long count, WritableByteChannel target) |
参数解析:
WritableByteChannel
可写通道,即目标通道,将当前通道数据传输到目标通道。ReadableByteChannel
可读通道,即源通道,源通道数据传输到当前通道。position
文件传输开始的位置。count
传输的最大字节数。
示例:
1 | RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); |
文件锁
文件锁定是一个复杂的操作,特别不同的操作系统是以不同的方式来实现锁的,为了尽可能保持代码的可移植性:
- 只使用排它锁
- 将所有的锁视为劝告式的
1 | // 文件锁,只能通过 FileChannel.java 来获取 |
将文件映射到内存
内存映射文件,是由一个文件到一块内存的映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行 I/O
操作,使得内存映射文件在处理大数据量的文件时能有很高的性能。
1 | public static class MapMode { |
字符集
字符集用来把 Unicode
字符编码和其它字符编码互转。Charset
是十六位 Unicode
字符序列与字节序列之间的一个命名的映射。要读和写文本,我们要分别使用 CharsetDecoder
和 CharsetEncoder
,即编码器和解码器。CharsetDecoder
用于将逐位表示的一串字符转换为具体的 char
值,而 CharsetEncoder
用于将字符转换回位。Java
实现都要求对以下字符编码提供完全的支持:
US-ASCII
ISO-8859-1
UTF-8
UTF-16BE
UTF-16LE
UTF-16
Pipe
NIO
管道是 2 个线程之间的单向数据连接。Pipe
有两个通道:
source
通道:数据从该通道读取sink
通道:数据从该通道写入
源码
1 | public static abstract class SourceChannel |
示例
1 | Pipe pipe = Pipe.open(); |
NIO
与 IO
的比较
简述
IO
:面向流,阻塞NIO
:面向缓冲区块操作,非阻塞
面向流和面向缓存区
- 面向流
面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。也不能前后移动流中的数据,如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 - 面向缓冲区
数据读取到缓冲区中,并可在缓冲区中前后移动,但是维护好缓冲区的状态变量。
阻塞和非阻塞
- 阻塞
流是阻塞的,在read, write
时线程被阻塞,直到有一些数据被读取或数据完全写入,该线程在此期间不能再干任何事情。 - 非阻塞
线程从通道读取或写入数据,但是当数据没准备好时直接返回不会阻塞。所以直至数据可用前,线程同时可以去做别的事情。线程通常将非阻塞IO
的空闲时间用于在其它通道上执行IO
操作,所以一个单独的线程现在可以管理多个输入和输出通道。
选择器
选择器允许单个线程监视多个通道,但是解析数据时会更复杂。常见应用为多连接但是传输少量数据,如聊天系统;或者少量连接大量数据用来做服务器。
参考文档
Java
源码- NIO 入门
- Java NIO系列教程