AIO的本质

66 阅读4分钟

在Java网络编程中,我们都在大量的讨论同步IO和异步IO的区别,具体就是谈论BIO和NIO,而对于AIO的讨论往往都是一笔带过。但这其实并不怪开源社区,2011年在Java7中就增加了AIO,号称是异步IO网络编程模型,但是这么久过去,各种开发框架和中间件中还是以NIO为主。AIO更多的被称为NIO2.0.像netty这样的网络框架都放弃了对AIO的支持。

1. 什么是异步

在Java编程中,我们通常将异步就等同于多线程,包括使用@Async将任务交给另一个线程执行实现异步,或者直接将任务抛给线程池都是同样的原理。归根到底,我们是将多个任务并行,让后者任务不阻塞前者任务,从而执行其他任务或者直接返回。认为这种两者的运行方式就是异步的实现。抽象一点来看,就是一个顺序性的问题,并行了就是异步,有上下文关系就是同步。

2. BIO和NIO是同步还是异步

背过八股文的都知道BIO和NIO都属于同步IO中的一种。那么这里提出一个疑问,我们通常设计网络框架的时候,都会将接收到的数据用另一个线程去处理,比如:

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

又或者使用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) {
 
            }
        });
    }
}

明明我们都使用了多线程,为什么还说他是同步IO呢???

是因为针对io读写这件事,启动另一个线程去处理数据已经是脱离了io读写的范围了,直白的说,他俩不是一件事了,读写io和处理数据你可以设计成同步,也可以开多线程设计成异步。

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

从上述的代码的运行结果来看,AIO的最大特点在于发起连接的线程和真正去接收数据的线程是不一致的。

因此,我们可以读出结论:发起io操作,比如accept、read、write调用的线程和最终完成这个操作的线程不是同一个线程的IO模式我们叫做AIO,更深层次的理解:同步异步模型是针对用户线程和内核的交互来说的,同步io就是需要内核将数据准备好后自己再去拷贝,而异步io则是自动拷贝数据到用户进程中,只需要发信号告知用户线程。

3. AIO的本质

image

从上面的流程来看,每次io读写都需要经历这三个阶段,那么就可能会出现死亡回调,在回调方法里添加另外一个回调,大大提高了编程的复杂度。

而且,这里面所谓的异步其实就是通过监听回调实现的。当监听到有事件发生就通知回调函数执行。那么就看看监听回调的本质是什么?

3.1 监听回调的本质

用户态和内核态的通信

用户态->内核态:通过系统调用实现。

内核态->用户态:内核态根本不知道用户态有什么函数,更不知道在哪,所以它只能通过发送信号来通信。比如kill命令。

所以说内核态是不能直接主动去执行用户态的回调函数的,所以所谓的回调函数其实就是用户态的自导自演,用户态既做了对内核态的监听,又做了回调函数,监听到信号后执行自己的回调函数。

所谓的监听回调的本质,就是用户态调用内核态的函数(read,write,epollAWait),改函数没有返回的时候用户线程阻塞了。当有函数返回的时候就会唤醒阻塞线程,执行所谓的回调函数。

由此可见AIO的本质只是在用户态实现了异步,并没有实际意义上的异步。

  • Java AIO和NIO一样,在不同的平台有不同的实现,在linux使用的是epoll,win使用的是IOCP,Mac使用的是KQueue。
  • netty废除AIO的原因是其性能上没有NIO高,linux虽然有一套原生的AIO实现(类似ICOP),但是JAVA AIO在Linux上并没有采用,而是使用了epoll。
  • Java AIO 不支持udp,同时编程较为复杂, 容易出现死亡回调。