到底什么是Java AIO?为什么Netty会移除AIO?一文搞懂AIO的本质!
1、引言关于Java网络编程中的同步IO和异步IO的区别及原理的文章非常的多,具体来说主要还是在讨论Java BIO和Java NIO这两者,而关于Java AIO的文章就少之又少了(即使用也只是介绍了一下概念和代码示例)。 在深入了解AIO之前,我注意到以下几个现象:
Java AIO的这些不合常理的现象难免会令人心存疑惑。所以决定写这篇文章时,我不想只是简单的把AIO的概念再复述一遍,而是要透过现象,深入分析、思考和并理解Java AIO的本质。 2、我们所理解的异步
public void create() { //TODO } public void build() { executor.execute(() -> build()); } 不管是用@Async注解,还是往线程池里提交任务,他们最终都是同一个结果,就是把要执行的任务,交给另外一个线程来执行。
这个时候,我们可以大致的认为,所谓的“异步”,就是用多线程的方式去并行执行任务。 3、Java BIO和NIO到底是同步还是异步?Java BIO和NIO到底是同步还是异步,我们先按照异步这个思路,做异步编程。 3.1BIO代码示例
InputStream in = socket.getInputStream(); in.read(data); // 接收到数据,异步处理 executor.execute(() -> handle(data)); public void handle( byte [] data) { // TODO } 如上:BIO在read()时,虽然线程阻塞了,但在收到数据时,可以异步启动一个线程去处理。
3.2NIO代码示例
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()可以阻塞等待数据,在有数据可读的时候,异步启动一个线程,去读取数据和处理数据。
3.3产生的理解偏差此时我们信誓旦旦地说,Java的BIO和NIO是异步还是同步,取决你的心情,你高兴给它个多线程,它就是异步的。 但果真如此么? 在翻阅了大量博客文章之后,基本一致的阐明了——BIO和NIO是同步的。 那问题点出在哪呢,是什么造成了我们理解上的偏差呢? 那就是参考系的问题,以前学物理时,公交车上的乘客是运动还是静止,需要有参考系前提,如果以地面为参考,他是运动的,以公交车为参考,他是静止的。 Java IO也是一样,需要有个参考系,才能定义它是同步还是异步。 既然我们讨论的是关于Java IO是哪一种模式,那就是要针对IO读写操作这件事来理解,而其他的启动另外一个线程去处理数据,已经是脱离IO读写的范围了,不应该把他们扯进来。 3.4尝试定义异步所以以IO读写操作这事件作为参照,我们先尝试的这样定义,就是:发起IO读写的线程(调用read和write的线程),和实际操作IO读写的线程,如果是同一个线程,就称之为同步,否则是异步。 按上述定义:
按照这个思路,AIO应该是发起IO读写的线程,和实际收到数据的线程,可能不是同一个线程。 是不是这样呢?我们将在上一节直接上Java AIO的代码,我们从 实际代码中一窥究竟吧。 4、一个Java AIO的网络编程示例4.1AIO服务端程序代码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) { } } 4.2AIO客户端程序
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); } } 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平台上运行,其他平台略有差异)。如下图所示。 分析线程栈,发现,程序启动了那么几个线程:
6、 AIO示例引发思考2:AIO注册事件监听和执行回调是如何实现的?
注:注册事件调用EPoll.ctl(...)函数,这个函数在最后的参数用于指定是一次性的,还是永久性。上面代码events | EPOLLONSHOT字面意思看来,是一次性的。 监听事件:
处理事件:
核心流程总结: 在分析完上面的代码流程后会发现:每一次IO读写都要经历的这三个事件是一次性的,也就是在处理事件完,本次流程就结束了,如果想继续下一次的IO读写,就得从头开始再来一遍。这样就会存在所谓的死亡回调(回调方法里再添加下一个回调方法),这对于编程的复杂度大大提高了。 7、 AIO示例引发思考3:监听回调的本质是什么?
7.1概述
7.2系统调用与函数调用
7.3用户态和内核态之间的通信
7.4用实际例子验证结论
定位到具体的代码上:可以看到“AWT-XAWT”正在做while循环,调用waitForEvents函数等待事件返回。如果没有事件,线程就一直阻塞在那边。如下图所示。
8、Java AIO的本质是什么?
8.1Java AIO的本质,就是只在用户态实现了异步
8.2Java AIO的其它真相
|