Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

2023-03-20 0 3,496

1. 前言

关于 Java BIO、NIO、AIO 的区别和原理,这样的文章非常的多的,但主要还是在 BIO 和 NIO 这两者之间讨论,而关于 AIO 这样的文章就少之又少了,很多只是介绍了一下概念和代码示例。

在了解 AIO 时,有注意到以下几个现象:

1、 2011 年 Java 7 发布,里面增加了 AIO 称之为异步 IO 的编程模型,但已经过去了近 12 年,平时使用的开发框架中间件,还是以 NIO 为主,例如网络框架 Netty、Mina,Web 容器 Tomcat、Undertow。

2、 Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

3、 Netty 舍去了 AIO 的支持。https://github.com/netty/netty/issues/2515

4、 AIO 看起来只是解决了有无,发布了个寂寞。
这几个现象不免会令很多人心存疑惑,所以决定写这篇文章时,不想简单的把 AIO 的概念再复述一遍,而是要透过现象, 如何分析、思考和理解 Java AIO 的本质。

2. 什么是异步

2.1 我们所了解的异步

AIO 的 A 是 Asynchronous 异步的意思,在了解 AIO 的原理之前,我们先理清一下 “异步” 到底是怎样的一个概念。
说起异步编程,在平时的开发还是比较常见,例如以下的代码示例:

@Async
public void create() {
    //TODO
}
​
public void build() {
    executor.execute(() -> build());
}

不管是用 @Async 注解,还是往线程池里提交任务,他们最终都是同一个结果,就是把要执行的任务,交给另外一个线程来执行。
这个时候,可以大致的认为,所谓的 “异步”,就是多线程,执行任务。

2.2 Java BIO 和 NIO 到底是同步还是异步?

Java BIO 和 NIO 到底是同步还是异步,我们先按照异步这个思路,做异步编程。

2.2.1 BIO 示例

byte [] data = new byte[1024];
InputStream in = socket.getInputStream();
in.read(data);
// 接收到数据,异步处理
executor.execute(() -> handle(data));
​
public void handle(byte [] data) {
    // TODO
}

BIO 在 read () 时,虽然线程阻塞了,但在收到数据时,可以异步启动一个线程去处理。

2.2.2 NIO 示例

selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
    SelectionKey key = iterator.next();
    if (key.isReadable()) {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
        executor.execute(() -> {
            try {
                channel.read(byteBuffer);
                handle(byteBuffer);
            } catch (Exception e) {
​
            }
        });
​
    }
}
​
public static void handle(ByteBuffer buffer) {
    // TODO
}

同理,NIO 虽然 read () 是非阻塞的,通过 select () 可以阻塞等待数据,在有数据可读的时候,异步启动一个线程,去读取数据和处理数据。

2.2.3 产生理解的偏差

此时我们信誓旦旦的说,Java 的 BIO 和 NIO 是异步还是同步,取决你的心情,你高兴给它个多线程,它就是异步的。

但果真如此么,在翻阅了大量博客文章之后,基本一致的阐明了,BIO 和 NIO 是同步的。

那问题点出在哪呢,是什么造成了我们理解上的偏差呢?

那就是参考系的问题,以前学物理时,公交车上的乘客是运动还是静止,需要有参考系前提,如果以地面为参考,他是运动的,以公交车为参考,他是静止的。

Java IO 也是一样,需要有个参考系,才能定义它是同步异步,既然我们讨论的是 IO 是哪一种模式,那就是要针对 IO 读写操作这件事来理解,而其他的启动另外一个线程去处理数据,已经是脱离 IO 读写的范围了,不应该把他们扯进来。

2.2.4 尝试定义异步

所以以 IO 读写操作这事件作为参照,我们先尝试的这样定义,就是发起 IO 读写的线程 (调用 read 和 write 的线程),和实际操作 IO 读写的线程,如果是同一个线程,就称之为同步,否则是异步

  • 显然 BIO 只能是同步,调用 in.read () 当前线程阻塞,有数据返回的时候,接收到数据的还是原来的线程。
  • 而 NIO 也称之为同步,原因也是如此,调用 channel.read () 时,线程虽然不会阻塞,但读到数据的还是当前线程。

按照这个思路,AIO 应该是发起 IO 读写的线程,和实际收到数据的线程,可能不是同一个线程
是不是这样呢,现在开始上 Java AIO 的代码。

2.3 Java AIO 的程序示例

2.3.1 AIO 服务端程序

public class AioServer {
​
    public static void main(String[] args) throws IOException {
        System.out.println(Thread.currentThread().getName() + " AioServer start");
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()
                .bind(new InetSocketAddress("127.0.0.1", 8080));
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
​
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
                System.out.println(Thread.currentThread().getName() + " client is connected");
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                clientChannel.read(buffer, buffer, new ClientHandler());
            }
​
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("accept fail");
            }
        });
        System.in.read();
    }
}
​
public class ClientHandler implements CompletionHandler<Integer, ByteBuffer> {
    @Override
    public void completed(Integer result, ByteBuffer buffer) {
        buffer.flip();
        byte [] data = new byte[buffer.remaining()];
        buffer.get(data);
        System.out.println(Thread.currentThread().getName() + " received:"  + new String(data, StandardCharsets.UTF_8));
    }
​
    @Override
    public void failed(Throwable exc, ByteBuffer buffer) {
​
    }
}

2.3.2 AIO 客户端程序

public class AioClient {
​
    public static void main(String[] args) throws Exception {
        AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
        channel.connect(new InetSocketAddress("127.0.0.1", 8080));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put("Java AIO".getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        Thread.sleep(1000L);
        channel.write(buffer);
 }
}

2.3.3 异步的定义猜想结论

分别运行服务端和客户端程序

Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

在服务端运行结果里,

main 线程发起 serverChannel.accept 的调用,添加了一个 CompletionHandler 监听回调,当有客户端连接过来时,Thread-5 线程执行了 accep 的 completed 回调方法。

紧接着 Thread-5 又发起了 clientChannel.read 调用,也添加了个 CompletionHandler 监听回调,当收到数据时,是 Thread-1 的执行了 read 的 completed 回调方法。

这个结论和上面异步猜想一致,发起 IO 操作(例如 accept、read、write) 调用的线程,和最终完成这个操作的线程不是同一个,我们把这种 IO 模式称之 AIO

当然了,这样定义 AIO 只是为了方便我们理解,实际中对异步 IO 的定义可能更抽象一点。

3.AIO 示例引发思考的问题

1、 执行 completed () 方法的这个线程是谁创建的,什么时候创建的?

2、 AIO 注册事件监听和执行回调是如何实现的?

3、 监听回调的本质是什么?

3.1 问题 1:执行 completed () 方法的这个线程是谁创建的,什么时候创建的

一般,这样的问题,需要从程序的入口的开始了解,但跟线程相关,其实是可以从线程栈的运行情况来定位线程是怎么运行。

只运行 AIO 服务端程序,客户端不运行,打印一下线程栈(备注:程序在 Linux 平台上运行,其他平台略有差异)

Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

分析线程栈,发现,程序启动了那么几个线程

1、 线程 Thread-0 阻塞在 EPoll.wait () 方法上

2、 线程 Thread-1、Thread-2。。。Thread-n(n 和 CPU 核心数量一致)从阻塞队列里 take () 任务,阻塞等待有任务返回。

此时可以暂定下一个结论:

AIO 服务端程序启动之后,就开始创建了这些线程,且线程都处于阻塞等待状态。

另外,发现这些线程的运行都跟 Epoll 有关系,提到 Epoll,我们印象中,Java NIO 在 Linux 平台底层就是用 Epoll 来实现的,难道 Java AIO 也是用 Epoll 来实现么?为了证实这个结论,我们从下一个问题来展开讨论

3.2 问题 2:AIO 注册事件监听和执行回调是如何实现的

带着这个问题,去阅读分析源码时,发现源码特别的长,而源码解析是一项枯燥乏味的过程,很容易把阅读者给逼走劝退掉。

对于长流程和逻辑复杂的代码的理解,我们可以抓住它几个脉络,找出哪几个核心流程。

以注册监听 read 为例 clientChannel.read (…),它主要的核心流程是:

1、注册事件 -> 2、监听事件 -> 3、处理事件

3.2.1 1、注册事件

Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

注册事件调用 EPoll.ctl (…) 函数,这个函数在最后的参数用于指定是一次性的,还是永久性。上面代码 events | EPOLLONSHOT 字面意思看来,是一次性的。

3.2.2 2、监听事件

Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

3.2.3 3、处理事件

Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?
Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?
Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

3.2.4 核心流程总结

Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

在分析完上面的代码流程后会发现,每一次 IO 读写都要经历的这三个事件是一次性的,也就是在处理事件完,本次流程就结束了,如果想继续下一次的 IO 读写,就得从头开始再来一遍。这样就会存在所谓的死亡回调(回调方法里再添加下一个回调方法),这对于编程的复杂度大大提高了。

3.3 问题 3: 监听回调的本质是什么?

先说一下结论,所谓监听回调的本质,就是用户态线程,调用内核态的函数(准确的说是 API,例如 read,write,epollWait),该函数还没有返回时,用户线程被阻塞了。当函数返回时,会唤醒阻塞的线程,执行所谓回调函数

对于这个结论的理解,要先引入几个概念

3.3.1 系统调用与函数调用

函数调用:

找到某个函数,并执行函数里的相关命令

系统调用:

操作系统对用户应用程序提供了编程接口,所谓 API。

系统调用执行过程:

1. 传递系统调用参数

2. 执行陷入指令,用用户态切换到核心态,这是因为系统调用一般都需要再核心态下执行

3. 执行系统调用程序

4. 返回用户态

3.3.2 用户态和内核态之间的通信

用户态 -> 内核态,通过系统调用方式即可。

内核态 -> 用户态,内核态根本不知道用户态程序有什么函数,参数是啥,地址在哪里。所以内核是不可能去调用用户态的函数,只能通过发送信号,比如 kill 命令关闭程序就是通过发信号让用户程序优雅退出的。

既然内核态是不可能主动去调用用户态的函数,为什么还会有回调呢,只能说这个所谓回调其实就是用户态的自导自演。它既做了监听,又做了执行回调函数。

3.3.3 用实际例子验证结论

为了验证这个结论是否有说服力,举个例子,平时开发写代码用的 IntelliJ IDEA,它是如何监听鼠标、键盘事件和处理事件的。

按照惯例,先打印一下线程栈,会发现鼠标、键盘等事件的监听是由 “AWT-XAWT” 线程负责的,处理事件则是 “AWT-EventQueue” 线程负责。

Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

定位到具体的代码上,可以看到 “AWT-XAWT” 正在做 while 循环,调用 waitForEvents 函数等待事件返回。如果没有事件,线程就一直阻塞在那边。

Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的?

4.Java AIO 的本质是什么?

1、由于内核态无法直接调用用户态函数,Java AIO 的本质,就是只在用户态实现异步。并没有达到理想意义上的异步。

理想中的异步

何谓理想意义上的异步?这里举个网购的例子

两个角色,消费者 A,快递员 B

  • A 在网上购物时,填好家庭地址付款提交订单,这个相当于注册监听事件
  • 商家发货,B 把东西送到 A 家门口,这个相当于回调。

A 在网上下完单,后续的发货流程就不用他来操心了,可以继续做其他事。B 送货也不关心 A 在不在家,反正就把货扔到家门口就行了,两个人互不依赖,互不相干扰

假设 A 购物是用户态来做,B 送快递是内核态来做,这种程序运行方式过于理想了,实际中实现不了。

现实中的异步

A 住的是高档小区,不能随意进去,快递只能送到小区门口。

A 买了一件比较重的商品,比如一台电视,因为 A 要上班不在家里,所以找了一个好友 C 帮忙把电视搬到他家。
A 出门上班前,跟门口的保安 D 打声招呼,说今天有一台电视送过来,送到小区门口时,请电话联系 C,让他过来拿。

  • 此时,A 下单并跟 D 打招呼,相当于注册事件。在 AIO 中就是 EPoll.ctl (…) 注册事件。
  • 保安在门口蹲着相当于监听事件,在 AIO 中就是 Thread-0 线程,做 EPoll.wait (…)
  • 快递员把电视送到门口,相当于有 IO 事件到达。
  • 保安通知 C 电视到了,C 过来搬电视,相当于处理事件。

在 AIO 中就是 Thread-0 往任务队列提交任务,

Thread-1 ~n 去取数据,并执行回调方法。

整个过程中,保安 D 必须一直蹲着,寸步不能离开,否则电视送到门口,就被人偷了。

好友 C 也必须在 A 家待着,受人委托,东西到了,人却不在现场,这有点失信于人。

所以实际的异步和理想中的异步,在互不依赖,互不干扰,这两点相违背了。保安的作用最大,这是他人生的高光时刻。

异步过程中的注册事件、监听事件、处理事件,还有开启多线程,这些过程的发起者全是用户态一手操办,所以说 Java AIO 只在用户态实现了异步,这个和 BIO、NIO 先阻塞,阻塞唤醒后开启异步线程处理的本质一致。

2、Java AIO 跟 NIO 一样,在各个平台的底层实现方式也不同,在 Linux 是用 EPoll,Windows 是 IOCP,Mac OS 是 KQueue。原理是大同小异,都是需要一个用户线程阻塞等待 IO 事件,一个线程池从队列里处理事件。

3、 Netty 之所以移除掉 AIO,很大的原因是在性能上 AIO 并没有比 NIO 高。Linux 虽然也有一套原生的 AIO 实现(类似 Windows 上的 IOCP),但 Java AIO 在 Linux 并没有采用,而是用 EPoll 来实现。

4、 Java AIO 不支持 UDP

5、 AIO 编程方式略显复杂,比如 “死亡回调”

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

1. JK下载官网所有资源来源于开发团队,加入会员即可下载使用!如有问题请联系右下角在线客服!
2. JK下载官方保障所有软件都通过人工亲测,为每位会员用户提供安全可靠的应用软件、游戏资源下载及程序开发服务。
3. JK开发团队针对会员诉求,历经多年拥有现今开发成果, 每款应用程序上线前都经过人工测试无误后提供安装使用,只为会员提供安全原创的应用。
4. PC/移动端应用下载后如遇安装使用问题请联系右下角在线客服或提交工单,一对一指导解决疑难。

JK软件下载官网 技术分享 Java AIO 又称为 NIO 2.0,难道它也是基于 NIO 来实现的? https://www.jkxiazai.com/772.html

JK软件应用商店是经过官方安全认证,保障正版软件平台

相关资源

官方客服团队

为您解决烦忧 - 24小时在线 专业服务