本文由得物技术团队Uni分享,本文有内容修订和大量排版优化。

1、引言

关于Java网络编程中的同步IO和异步IO的区别及原理的文章非常的多,具体来说主要还是在讨论Java BIO和Java NIO这两者,而关于Java 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的支持?(点此查看);
  • 4)AIO看起来貌似只是解决了有无,实际是发布了个寂寞?

Java AIO的这些不合常理的现象难免会令人心存疑惑。所以决定写这篇文章时,我不想只是简单的把AIO的概念再复述一遍,而是要透过现象,深入分析、思考和并理解Java AIO的本质。

 

技术交流:

(本文已同步发布于:http://www.52im.net/thread-4283-1-1.html

2、我们所理解的异步

AIO的A是Asynchronous(即异步)的意思,在了解AIO的原理之前,我们先理清一下“异步”到底是怎样的一个概念。

说起异步编程,在平时的开发还是比较常见的。

例如以下的代码示例:

@Async

publicvoidcreate() {

//TODO

}

publicvoidbuild() {

executor.execute(() -> build());

}

不管是用@Async注解,还是往线程池里提交任务,他们最终都是同一个结果,就是把要执行的任务,交给另外一个线程来执行。

这个时候,我们可以大致的认为,所谓的“异步”,就是用多线程的方式去并行执行任务。

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

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

3.1BIO代码示例

byte[] data = newbyte[1024];

InputStream in = socket.getInputStream();

in.read(data);

// 接收到数据,异步处理

executor.execute(() -> handle(data));

publicvoidhandle(byte[] data) {

// TODO

}

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

3.2NIO代码示例

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) {

}

});

}

}

publicstaticvoidhandle(ByteBuffer buffer) {

// TODO

}

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

3.3产生的理解偏差

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

果真如此么?

在翻阅了大量博客文章之后,基本一致的阐明了——BIO和NIO是同步的。

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

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

Java IO也是一样,需要有个参考系,才能定义它是同步还是异步。

既然我们讨论的是关于Java IO是哪一种模式,那就是要针对IO读写操作这件事来理解,而其他的启动另外一个线程去处理数据,已经是脱离IO读写的范围了,不应该把他们扯进来。

3.4尝试定义异步

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

按上述定义:

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

按照这个思路,AIO应该是发起IO读写的线程,和实际收到数据的线程,可能不是同一个线程。

是不是这样呢?我们将在上一节直接上Java AIO的代码,我们从 实际代码中一窥究竟吧。

4、一个Java AIO的网络编程示例

4.1AIO服务端程序代码

publicclassAioServer {

publicstaticvoidmain(String[] args) throwsIOException {

System.out.println(Thread.currentThread().getName() + " AioServer start");

AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()

.bind(newInetSocketAddress("127.0.0.1", 8080));

serverChannel.accept(null, newCompletionHandler<AsynchronousSocketChannel, Void>() {

@Override

publicvoidcompleted(AsynchronousSocketChannel clientChannel, Void attachment) {

System.out.println(Thread.currentThread().getName() + " client is connected");

ByteBuffer buffer = ByteBuffer.allocate(1024);

clientChannel.read(buffer, buffer, newClientHandler());

}

@Override

publicvoidfailed(Throwable exc, Void attachment) {

System.out.println("accept fail");

}

});

System.in.read();

}

}

publicclassClientHandler implementsCompletionHandler<Integer, ByteBuffer> {

@Override

publicvoidcompleted(Integer result, ByteBuffer buffer) {

buffer.flip();

byte[] data = newbyte[buffer.remaining()];

buffer.get(data);

System.out.println(Thread.currentThread().getName() + " received:"+ newString(data, StandardCharsets.UTF_8));

}

@Override

publicvoidfailed(Throwable exc, ByteBuffer buffer) {

}

}

4.2AIO客户端程序

publicclassAioClient {

publicstaticvoidmain(String[] args) throwsException {

AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();

channel.connect(newInetSocketAddress("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);

}

}

4.3异步的定义猜想结论

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

在服务端运行结果里:

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平台上运行,其他平台略有差异)。如下图所示。

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

  • 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来实现么?

为了证实这个结论,我们从下一个问题来展开讨论。

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系统调用与函数调用

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

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

系统调用执行过程:

  • 1)传递系统调用参数;
  • 2)执行陷入指令,用用户态切换到核心态(这是因为系统调用一般都需要再核心态下执行);
  • 3)执行系统调用程序;
  • 4)返回用户态。

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:

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

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

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

2)现实中的异步:

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

A买了一件比较重的商品,比如一台电视,因为A要上班不在家里,所以找了一个好友C帮忙把电视搬到他家。

A出门上班前,跟门口的保安D打声招呼,说今天有一台电视送过来,送到小区门口时,请电话联系C,让他过来拿。

具体就是:

  • 1)此时,A下单并跟D打招呼,相当于注册事件。在AIO中就是EPoll.ctl(...)注册事件;
  • 2)保安在门口蹲着相当于监听事件,在AIO中就是Thread-0线程,做EPoll.wait(..);
  • 3)快递员把电视送到门口,相当于有IO事件到达;
  • 4)保安通知C电视到了,C过来搬电视,相当于处理事件(在AIO中就是Thread-0往任务队列提交任务,Thread-1 ~n去取数据,并执行回调方法)。

整个过程中,保安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编程方式略显复杂,比如“死亡回调”。

9、参考资料

[1] 少啰嗦!一分钟带你读懂Java的NIO和经典IO的区别

[2] 史上最强Java NIO入门:担心从入门到放弃的,请读这篇!

[3] Java的BIO和NIO很难懂?用代码实践给你看,再不懂我转行!

[4] Java新一代网络编程模型AIO原理及Linux系统AIO介绍

[5] 从0到1的快速裂变:详解快的打车架构设计及技术实践

[6] 新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

[7] 史上最通俗Netty框架入门长文:基本介绍、环境搭建、动手实战

[8] 高性能网络编程(五):一文读懂高性能网络编程中的I/O模型

[9] 高性能网络编程(六):一文读懂高性能网络编程中的线程模型

[10] 高性能网络编程(七):到底什么是高并发?一文即懂!

[11] 从根上理解高性能、高并发(二):深入操作系统,理解I/O与零拷贝技术

[12] 从根上理解高性能、高并发(三):深入操作系统,彻底理解I/O多路复用

[13] 从根上理解高性能、高并发(四):深入操作系统,彻底理解同步与异步

[14] 从根上理解高性能、高并发(五):深入操作系统,理解高并发中的协程

(本文已同步发布于:http://www.52im.net/thread-4283-1-1.html

到底什么是Java AIO?为什么Netty会移除AOI?一文搞懂AIO的本质!的更多相关文章

  1. 一文搞懂所有Java集合面试题

    Java集合 刚刚经历过秋招,看了大量的面经,顺便将常见的Java集合常考知识点总结了一下,并根据被问到的频率大致做了一个标注.一颗星表示知识点需要了解,被问到的频率不高,面试时起码能说个差不多.两颗 ...

  2. 一文搞懂Java引用拷贝、浅拷贝、深拷贝

    微信搜一搜 「bigsai」 专注于Java和数据结构与算法的铁铁 文章收录在github/bigsai-algorithm 在开发.刷题.面试中,我们可能会遇到将一个对象的属性赋值到另一个对象的情况 ...

  3. 一文搞懂Java引用拷贝、深拷贝、浅拷贝

    刷题.面试中,我们可能会遇到将一个对象的属性赋值到另一个对象的情况,这种情况就叫做拷贝.拷贝与Java内存结构息息相关,搞懂Java深浅拷贝是很必要的! 在对象的拷贝中,很多初学者可能搞不清到底是拷贝 ...

  4. 一文搞懂 Netty 发送数据全流程 | 你想知道的细节全在这里

    欢迎关注公众号:bin的技术小屋,如果大家在看文章的时候发现图片加载不了,可以到公众号查看原文 本系列Netty源码解析文章基于 4.1.56.Final版本 在<Netty如何高效接收网络数据 ...

  5. 一文搞懂 Java 线程中断

    在之前的一文<如何"优雅"地终止一个线程>中详细说明了 stop 终止线程的坏处及如何优雅地终止线程,那么还有别的可以终止线程的方法吗?答案是肯定的,它就是我们今天要分 ...

  6. 夯实Java基础系列6:一文搞懂抽象类和接口,从基础到面试题,揭秘其本质区别!

    目录 抽象类介绍 为什么要用抽象类 一个抽象类小故事 一个抽象类小游戏 接口介绍 接口与类相似点: 接口与类的区别: 接口特性 抽象类和接口的区别 接口的使用: 接口最佳实践:设计模式中的工厂模式 接 ...

  7. 一文搞懂--Java中重写equals方法为什么要重写hashcode方法?

    Java中重写equals方法为什么要重写hashcode方法? 直接看下面的例子: 首先我们只重写equals()方法 public class Test { public static void ...

  8. Java虚拟机系列一:一文搞懂 JVM 架构和运行时数据区

    前言 之前写博客一直比较随性,主题也很随意,就是想到什么写什么,对什么感兴趣就写什么.虽然写起来无拘无束,自在随意,但也带来了一些问题,每次写完一篇后就要去纠结下一篇到底写什么,看来选择太多也不是好事 ...

  9. Java基础-一文搞懂位运算

    在日常的Java开发中,位运算使用的不多,使用的更多的是算数运算(+.-.*./.%).关系运算(<.>.<=.>=.==.!=)和逻辑运算(&&.||.!), ...

  10. 一文搞懂Java环境,轻松实现Hello World!

    在上篇文章中,我们介绍了Java自学大概的路线.然而纸上得来终觉浅,今天我们教大家写第一个java demo.(ps:什么是demo?Demo的中文含意为“示范",Demo源码可以理解为某种 ...

随机推荐

  1. 云原生周刊:Docker 推出 Docker Debug | 2023.10.9

    开源项目推荐 SchemaHero SchemaHero 是一个 Kubernetes Operator,用于各种数据库的声明式架构管理.SchemaHero 有以下目标: 数据库表模式可以表示为可以 ...

  2. 云原生爱好者周刊:玩 Kubernetes 游戏,赢取免费机票

    云原生一周动态要闻: Grafana 8.2.2 发布 OSM(Open Service Mesh)发布 v1.0 的第一个候选版本 谷歌宣布推出 Google Distributed Cloud K ...

  3. 一次彻底讲清如何处理mysql 的死锁问题

    MySQL 死锁 是指两个或多个事务互相等待对方持有的锁,从而导致所有事务都无法继续执行的现象.在 InnoDB 存储引擎中,死锁是通过锁机制产生的,特别是在并发较高.业务逻辑复杂的情况下,更容易发生 ...

  4. 分支定界方法(branch and cut,branch and price的基础)

    分支定界方法(branch and cut,branch and price的基础) 目录 1.基础版的分支定界算法(假设是min问题) 2.分支定界算法的步骤及其注意事项 2.1 具体的分支定界方法 ...

  5. fiddler限速配置&mock配置

    一.限速配置 1.开启性能选项 2.找到对应的参数入口 3.修改对应的从参数 解释下 这2个参数是如何做到限速的 4.request-trickle-delay(上传数据限制) 默认值是300,他的意 ...

  6. Windows 多次制作母盘,备份文件变大的问题

    公司产品基于Win11 23H2镜像版本制作母盘,我们发现随着版本迭代,基于上一版本母盘生成新母盘备份,母盘文件会越来越大. 此处说明下镜像与母盘文件的区别, 1. 镜像是指操作系统的压缩文件,常见格 ...

  7. 今日一学,5道大厂的Java基础面试题

    前言 各种框架眼花缭乱,各种逻辑需求,CRUD.久而久之,写的1000行代码中都是if else,@autowired等等,等出去面试的时候,基础题不断,而且还是不常用,或者说不在意的,往往这些就容易 ...

  8. golang工具之generate

    示例: 大家经常碰到命名错误码.状态码的同时,又要同步写码对应的翻译,有没有感觉很无聊.这里举一个例子: package main ​ import "fmt" ​ // 定义错误 ...

  9. Centos7下的开机自启动

    记录服务器每次重启之后启动一些服务. systemd systemd简介 CentOS7已不再使用chkconfig管理启动项,而是使用systemd.关于systemd的衍生和发展,可以参见< ...

  10. .net core想到哪写道哪之hello world

    今天,我们来创建一个helo world,讲一讲.Net 6最新的顶级语句的问题. 在.Net 6中最大的变化应该就是多了个顶级语句. 这玩意是个啥呢,它让C#看起来像个脚本语言了,一个Hello W ...