Java 提供了 BIO、NIO 和 NIO.2 这些 API 来实现这些 I/O 模型。BIO 是我们最熟悉的同步阻塞,NIO 是同步非阻塞,那 NIO.2 又是什么呢?NIO 已经足够好了,为什么还要 NIO.2 呢?
NIO 和 NIO.2 最大的区别是,一个是同步一个是异步。我在上期提到过,异步最大的特点是,应用程序不需要自己去触发数据从内核空间到用户空间的拷贝。
为什么是应用程序去“触发”数据的拷贝,而不是直接从内核拷贝数据呢?这是因为应用程序是不能访问内核空间的,因此数据拷贝肯定是由内核来做,关键是谁来触发这个动作。
是内核主动将数据拷贝到用户空间并通知应用程序。还是等待应用程序通过 Selector 来查询,当数据就绪后,应用程序再发起一个 read 调用,这时内核再把数据从内核空间拷贝到用户空间。
需要注意的是,数据从内核空间拷贝到用户空间这段时间,应用程序还是阻塞的。所以你会看到异步的效率是高于同步的,因为异步模式下应用程序始终不会被阻塞。下面我以网络数据读取为例,来说明异步模式的工作过程。
- 首先,应用程序在调用 read API 的同时告诉内核两件事情:数据准备好了以后拷贝到哪个 Buffer,以及调用哪个回调函数去处理这些数据。
- 之后,内核接到这个 read 指令后,等待网卡数据到达,数据到了后,产生硬件中断,内核在中断程序里把数据从网卡拷贝到内核空间,接着做 TCP/IP 协议层面的数据解包和重组,再把数据拷贝到应用程序指定的 Buffer,最后调用应用程序指定的回调函数。
你可能通过下面这张图来回顾一下同步与异步的区别:
我们可以看到在异步模式下,应用程序当了“甩手掌柜”,内核则忙前忙后,但最大限度提高了 I/O 通信的效率。Windows 的 IOCP 和 Linux 内核 2.6 的 AIO 都提供了异步 I/O 的支持,Java 的 NIO.2 API 就是对操作系统异步 I/O API 的封装。
Java NIO.2 回顾
今天我们会重点关注 Tomcat 是如何实现异步 I/O 模型的,但在这之前,我们先来简单回顾下如何用 Java 的 NIO.2 API 来编写一个服务端程序。
public class Nio2Server {
void listen(){
//1.创建一个线程池
ExecutorService es = Executors.newCachedThreadPool();
//2.创建异步通道群组
AsynchronousChannelGroup tg = AsynchronousChannelGroup.withCachedThreadPool(es, 1);
//3.创建服务端异步通道
AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open(tg);
//4.绑定监听端口
assc.bind(new InetSocketAddress(8080));
//5. 监听连接,传入回调类处理连接请求
assc.accept(this, new AcceptHandler());
}
}
上面的代码主要做了 5 件事情:
- 创建一个线程池,这个线程池用来执行来自内核的回调请求。
- 创建一个 AsynchronousChannelGroup,并绑定一个线程池。
- 创建 AsynchronousServerSocketChannel,并绑定到 AsynchronousChannelGroup。
- 绑定一个监听端口。
- 调用 accept 方法开始监听连接请求,同时传入一个回调类去处理连接请求。请你注意,accept 方法的第一个参数是 this 对象,就是 Nio2Server 对象本身,我在下文还会讲为什么要传入这个参数。
你可能会问,为什么需要创建一个线程池呢?其实在异步 I/O 模型里,应用程序不知道数据在什么时候到达,因此向内核注册回调函数,当数据到达时,内核就会调用这个回调函数。同时为了提高处理速度,会提供一个线程池给内核使用,这样不会耽误内核线程的工作,内核只需要把工作交给线程池就立即返回了。
我们再来看看处理连接的回调类 AcceptHandler 是什么样的。
//AcceptHandler类实现了CompletionHandler接口的completed方法。它还有两个模板参数,第一个是异步通道,第二个就是Nio2Server本身
public class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Nio2Server> {
//具体处理连接请求的就是completed方法,它有两个参数:第一个是异步通道,第二个就是上面传入的NioServer对象
@Override
public void completed(AsynchronousSocketChannel asc, Nio2Server attachment) {
//调用accept方法继续接收其他客户端的请求
attachment.assc.accept(attachment, this);
//1. 先分配好Buffer,告诉内核,数据拷贝到哪里去
ByteBuffer buf = ByteBuffer.allocate(1024);
//2. 调用read函数读取数据,除了把buf作为参数传入,还传入读回调类
channel.read(buf, buf, new ReadHandler(asc));
}
我们看到它实现了 CompletionHandler 接口,下面我们先来看看 CompletionHandler 接口的定义。