当应用程序发起 IO 系统调用后,会经历两个阶段:
- 内核等待 I/O 设备准备好数据。
- 内核将数据从内核空间拷贝到用户空间。
BIO
BIO(Blocking IO)属于同步阻塞 IO,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。BIO 因为应用程序一直阻塞,直到系统调用返回,所以效率不高。
NIO
NIO(Non-blocking/New IO),可以看作是多路复用 IO,Java 在 1.4 中引入 java.nio
包,提供了 Channel
, Selector
,Buffer
等抽象。
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
NIO 核心组件
Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
Channel(通道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。
Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。
类比于 Golang:
- Channel 和 Buffer:可以看作是 channel 和 channel 的缓冲区。
- Selecter:go select
Buffer
在 Java nio 库中,所有数据都是用缓冲区处理的。使用 NIO 在读写数据时,都是通过缓冲区进行操作。
Buffer
的子类如下图所示。其中,最常用的是 ByteBuffer
,它可以用来存储和操作字节数据。
Buffer 有四个成员变量:
- 容量 capacity:Buffer 可以存储的最大容量,创建时设置且不可改变。
- 界限 limit:Buffer 中可以读/写数据的边界。写模式下,limit 表示最多能写入的数据;读模式下,limit 表示最多能读取的数据。
- 位置 position:下一个可以被读写的数据的位置。从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。从读模式到写模式(clear),position 会归零,limit 设置为 capacity。
- 标记 mark:Buffer 允许将位置直接定位到该标记处,这是一个可选属性
Buffer 有读模式和写模式这两种模式,分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。Buffer 被创建之后默认是写模式,调用 flip 可以切换到读模式。如果要再次切换回写模式,可以调用 clear 或者 compact 方法。
Buffer
对象不能通过 new
调用构造方法创建对象 ,只能通过静态方法实例化 Buffer
。
1 | // 分配堆内存 |
Channel
Channel 是一个通道,它建立了与数据源(如文件、网络套接字等)之间的连接。Channel 和 Buffer 交互,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写。
Channel 的子类如下所示:
其中,最常用的是以下几种类型的通道:
FileChannel
:文件访问通道;SocketChannel
、ServerSocketChannel
:TCP 通信通道;DatagramChannel
:UDP 通信通道;
Channel 最核心的两个方法:
read
:读取数据并写入到 Buffer 中。write
:将 Buffer 中的数据写入到 Channel 中。
1 | RandomAccessFile reader = new RandomAccessFile("/Users/guide/Documents/test_read.in", "r")) |
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
会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。
Selector 还提供了一系列和 select()
相关的方法:
int select()
:监控所有注册的Channel
,当它们中间有需要处理的IO
操作时,该方法返回,并将对应的SelectionKey
加入被选择的SelectionKey
集合中,该方法返回这些Channel
的数量。int select(long timeout)
:可以设置超时时长的select()
操作。int selectNow()
:执行一个立即返回的select()
操作,相对于无参数的select()
方法而言,该方法不会阻塞线程。Selector wakeup()
:使一个还未返回的select()
方法立刻返回.
1 | import java.io.IOException; |
NIO 直接读取 零拷贝
直接读取
MappedByteBuffer
是 NIO 基于内存映射(mmap
)这种零拷贝方式的提供的⼀种实现,底层实际是调用了 Linux 内核的 mmap
系统调用。
它可以将一个文件或者文件的一部分映射到内存中,形成一个虚拟内存文件,这样就可以直接操作内存中的数据,而不需要通过系统调用来读写文件。
1 | private void loadFileIntoMemory(File xmlFile) throws IOException { |
零拷贝
FileChannel
的transferTo()/transferFrom()
是 NIO 基于发送文件(sendfile
)这种零拷贝方式的提供的一种实现,底层实际是调用了 Linux 内核的 sendfile
系统调用。
它可以直接将文件数据从磁盘发送到网络,而不需要经过用户空间的缓冲区。
AIO
AIO(Asynchronous IO)异步 IO,Java 7 中引入。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。