1、引言 关于Java网络编程中的同步IO和异步IO的区别及原理的文章非常的多,具体来说主要还是在讨论Java BIO和Java NIO这两者,而关于Java AIO的文章就少之又少了(即使用也只是介绍了一下概念和代码示例)。 在深入了解AIO之前,我注意到以下几个现象:
Java AIO的这些不合常理的现象难免会令人心存疑惑。所以决定写这篇文章时,我不想只是简单的把AIO的概念再复述一遍,而是要透过现象,深入分析、思考和并理解Java AIO的本质。 2、我们所理解的异步 AIO的A是Asynchronous(即异步)的意思,在了解AIO的原理之前,我们先理清一下“异步”到底是怎样的一个概念。 说起异步编程,在平时的开发还是比较常见的。 例如以下的代码示例:
这个时候,我们可以大致的认为,所谓的“异步”,就是用多线程的方式去并行执行任务。 3.1BIO代码示例
3.2NIO代码示例
3.3产生的理解偏差 但果真如此么? 在翻阅了大量博客文章之后,基本一致的阐明了——BIO和NIO是同步的。 那问题点出在哪呢,是什么造成了我们理解上的偏差呢? 那就是参考系的问题,以前学物理时,公交车上的乘客是运动还是静止,需要有参考系前提,如果以地面为参考,他是运动的,以公交车为参考,他是静止的。 Java IO也是一样,需要有个参考系,才能定义它是同步还是异步。 既然我们讨论的是关于Java IO是哪一种模式,那就是要针对IO读写操作这件事来理解,而其他的启动另外一个线程去处理数据,已经是脱离IO读写的范围了,不应该把他们扯进来。 3.4尝试定义异步 按上述定义:
按照这个思路,AIO应该是发起IO读写的线程,和实际收到数据的线程,可能不是同一个线程。 是不是这样呢?我们将在上一节直接上Java AIO的代码,我们从 实际代码中一窥究竟吧。
在服务端运行结果里: 1)main线程发起serverChannel.accept的调用,添加了一个CompletionHandler监听回调,当有客户端连接过来时,Thread-5线程执行了accep的completed回调方法。 2)紧接着Thread-5又发起了clientChannel.read调用,也添加了个CompletionHandler监听回调,当收到数据时,是Thread-1的执行了read的completed回调方法。 这个结论和上面异步猜想一致:发起IO操作(例如accept、read、write)调用的线程,和最终完成这个操作的线程不是同一个,我们把这种IO模式称之AIO。 当然了,这样定义AIO只是为了方便我们理解,实际中对异步IO的定义可能更抽象一点。5、 AIO示例引发思考1:“执行completed()方法的线程是谁创建、什么时候创建?” 只运行AIO服务端程序,客户端不运行,打印一下线程栈(备注:程序在Linux平台上运行,其他平台略有差异)。如下图所示。 分析线程栈,发现,程序启动了那么几个线程:
此时可以暂定下一个结论:AIO服务端程序启动之后,就开始创建了这些线程,且线程都处于阻塞等待状态。 另外:发现这些线程的运行都跟epoll有关系! 提到epoll,我们印象中,Java NIO在Linux平台底层就是用epoll来实现的,难道Java AIO也是用epoll来实现么? 为了证实这个结论,我们从下一个问题来展开讨论。 6、 AIO示例引发思考2:AIO注册事件监听和执行回调是如何实现的? 带着这个问题,去阅读JDK分析源码时,发现源码特别的长,而源码解析是一项枯燥乏味的过程,很容易把阅读者给逼走劝退掉。 对于长流程和逻辑复杂的代码的理解,我们可以抓住它几个脉络,找出哪几个核心流程。 以注册监听read为例clientChannel.read(…),它主要的核心流程是:注册事件 -> 监听事件 -> 处理事件。 注:注册事件调用EPoll.ctl(…)函数,这个函数在最后的参数用于指定是一次性的,还是永久性。上面代码events | EPOLLONSHOT字面意思看来,是一次性的。 监听事件: 处理事件: 核心流程总结: 在分析完上面的代码流程后会发现:每一次IO读写都要经历的这三个事件是一次性的,也就是在处理事件完,本次流程就结束了,如果想继续下一次的IO读写,就得从头开始再来一遍。这样就会存在所谓的死亡回调(回调方法里再添加下一个回调方法),这对于编程的复杂度大大提高了。 7、 AIO示例引发思考3:监听回调的本质是什么?7.1概述 先说一下结论:所谓监听回调的本质,就是用户态线程调用内核态的函数(准确的说是API,例如read、write、epollWait),该函数还没有返回时,用户线程被阻塞了。当函数返回时,会唤醒阻塞的线程,执行所谓回调函数。 对于这个结论的理解,要先引入几个概念。 7.2系统调用与函数调用 函数调用:找到某个函数,并执行函数里的相关命令。 系统调用执行过程:
7.3用户态和内核态之间的通信 用户态->内核态:通过系统调用方式即可。 内核态->用户态:内核态根本不知道用户态程序有什么函数,参数是啥,地址在哪里。所以内核是不可能去调用用户态的函数,只能通过发送信号,比如kill 命令关闭程序就是通过发信号让用户程序优雅退出的。 既然内核态是不可能主动去调用用户态的函数,为什么还会有回调呢,只能说这个所谓回调其实就是用户态的自导自演。它既做了监听,又做了执行回调函数。 7.4用实际例子验证结论 为了验证这个结论是否有说服力,举个例子:平时开发写代码用的IntelliJ IDEA,它是如何监听鼠标、键盘事件和处理事件的。 按照惯例,先打印一下线程栈,会发现鼠标、键盘等事件的监听是由“AWT-XAWT”线程负责的,处理事件则是“AWT-EventQueue”线程负责。如下图所示。 定位到具体的代码上:可以看到“AWT-XAWT”正在做while循环,调用waitForEvents函数等待事件返回。如果没有事件,线程就一直阻塞在那边。如下图所示。 8、Java AIO的本质是什么?8.1Java AIO的本质,就是只在用户态实现了异步 由于内核态无法直接调用用户态函数,Java AIO的本质,就是只在用户态实现异步,并没有达到理想意义上的异步。 1)理想中的异步: 何谓理想意义上的异步?这里举个网购的例子。 两个角色,消费者A、快递员B:
A在网上下完单,后续的发货流程就不用他来操心了,可以继续做其他事。B送货也不关心A在不在家,反正就把货扔到家门口就行了,两个人互不依赖,互不相干扰。 假设A购物是用户态来做,B送快递是内核态来做,这种程序运行方式过于理想了,实际中实现不了。 2)现实中的异步: A住的是高档小区,不能随意进去,快递只能送到小区门口。 A买了一件比较重的商品,比如一台电视,因为A要上班不在家里,所以找了一个好友C帮忙把电视搬到他家。 A出门上班前,跟门口的保安D打声招呼,说今天有一台电视送过来,送到小区门口时,请电话联系C,让他过来拿。 具体就是:
整个过程中,保安D必须一直蹲着,寸步不能离开,否则电视送到门口,就被人偷了。 好友C也必须在A家待着,受人委托,东西到了,人却不在现场,这有点失信于人。 所以实际的异步和理想中的异步,在互不依赖,互不干扰,这两点相违背了。保安的作用最大,这是他人生的高光时刻。 异步过程中的注册事件、监听事件、处理事件,还有开启多线程,这些过程的发起者全是用户态一手操办。所以说Java AIO本质只是在用户态实现了异步,这个和BIO、NIO先阻塞,阻塞唤醒后开启异步线程处理的本质一致。 8.2Java AIO的其它真相 Java AIO跟NIO一样:在各个平台的底层实现方式也不同,在Linux是用epoll、Windows是IOCP、Mac OS是KQueue。原理是大同小异,都是需要一个用户线程阻塞等待IO事件,一个线程池从队列里处理事件。 Netty之所以移除掉AIO:很大的原因是在性能上AIO并没有比NIO高。Linux虽然也有一套原生的AIO实现(类似Windows上的IOCP),但Java AIO在Linux并没有采用,而是用epoll来实现。 Java AIO不支持UDP。 AIO编程方式略显复杂,比如“死亡回调”。 |