在前两篇文章中,我们深入挖掘了NIO的三个套件:Channel(通道)、Buffer(缓冲区)和 Selector(选择器)。

NIO虽然通过多路复用提高了并发处理能力,但他还是同步的。不用阻塞了,还是需要我们轮询(Selector)去看数据好了没。

本文我们要讲的AIO,也就是NIO.2,是Java 7引入的真正的异步非阻塞I/O。

如果说BIO是傻等,NIO是轮询,那AIO就的算得上是甩手掌柜了。

一、什么是AIO

1.1 基础概念

AIO的全称是Asynchronous I/O,中午叫异步I/O,在Java 7中随NIO.2 (JSR 203) 引入。

AIO的核心思想是应用发起I/O操作之后,马上就返回,不阻塞也不轮询。

当操作系统完成I/O操作后,会通过回调函数通知应用程序。

 

我们用一个烧开水泡茶的过程来理解一下BIO、NIO、AIO。

使用BIO(阻塞 I/O)的情况,好比我们把水壶放到火上,站在旁边一直盯着,直到水开了才去泡茶。 全程不能干别的事。

如果使用NIO(非阻塞 I/O + 多路复用)就好比我们把水壶放到火上,然后去客厅看电视。每过一分钟,我们跑回厨房看一眼水开了没。不用一直站着,但要不断切换上下文(跑来跑去)去看水开了没。

如果使用AIO(异步 I/O)就好比我们买了一个会响的高级水壶。把他放到火上,我们就去睡觉了。水开了,水壶会自动响。这里就类似回调通知,然后我们去关火泡茶就行了。中间完全不管,等结果自己送上门。

1.2 核心模式

先来看一下Reactor和Proactor这两个单词,直译过来前者叫反应器,后者叫前摄器。

这两个东西对应着IO中的两种模式,前者强调的是reactive(反应),后者强调的是proactive(主动)。

Reactor模式就是非阻塞的同步I/O,核心是事件驱动,被动响应,当事件就绪时,通知我们,然后由应用程序自己执行实际的I/O操作。从描述就能看出这是NIO。

基于Reactor模式的NIO就是当事件(可读、可写)准备好时,通知应用程序。应用程序需要自己负责读取数据这个时候数据还在内核缓冲区,需要我们copy到用户缓冲区。

Proactor模式就是异步I/O。核心是操作驱动,主动完成,我们发起一个I/O操作,当操作完全完成时,系统会通知我们。这就是我们的AIO。

而基于Proactor模式的AIO应用程序告诉操作系统,我要读数据,读好后放在这个Buffer里,然后通知我。当通知到达时,操作已经完成,数据已经实实在在地在我们的Buffer里了。

二、核心类

AIO的类都在java.nio.channels包下,主要以Asynchronous开头。

 

2.1 异步服务器套接字通道

AsynchronousServerSocketChannel是异步的服务端监听通道,对应传统的ServerSocketChannel。

核心方法是:

bind(SocketAddress local)

accept(A attachment,CompletionHandler<AsynchronousSocketChannel,? super A> handler)

 

我们调用accept方法,传入一个CompletionHandler是非阻塞的,会马上返回。

如果有新的客户端建立连接的时候,AIO框架会内部完成连接的接受。

接受完成之后,系统在一个线程池里调用我们提供的CompletionHandler的completed方法,把新建立的 AsynchronousSocketChannel(代表客户端连接)作为参数传给我们。


package com.lazy.snail.day64;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

/**
 * @ClassName AsscDemo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/11/26 15:14
 * @Version 1.0
 */
public class AsscDemo {
    public static void main(String[] args) throws IOException {
        AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
        server.bind(new InetSocketAddress(8080));

        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel client, Void attachment) {
                System.out.println("客户端连接: " + client);
                server.accept(null, this);

                // TODO: 可以在这里进行具体的I/O操作
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
            }
        });
        // 阻塞主线程,防止程序立即退出(因为上面的accept是异步的)
        System.in.read();
    }
}

2.2 异步套接字通道

AsynchronousSocketChannel是异步的客户端通信通道,对应传统的SocketChannel。既可以用于客户端,也可以用于服务端接受的客户端连接。

核心的方法:

connect(SocketAddress remote)

read(ByteBuffer dst,A attachment,CompletionHandler<Integer,? super A> handler)

write(ByteBuffer src,A attachment,CompletionHandler<Integer,? super A> handler)

 

当我们调用read方法,传入一个空的ByteBuffer和一个CompletionHandler,会立即返回。

操作系统在后台把网络数据读取到我们提供的ByteBuffer里。

当整个读操作完成(缓冲区被填满,或到达流的末尾,或发生错误),系统在一个线程池中调用我们的 CompletionHandler。

在completed方法中,我们可以从ByteBuffer中获取已经读好的数据。

服务端的示例代码中关于I/O操作部分我们可以补全了:


package com.lazy.snail.day64;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;

/**
 * @ClassName AsscDemo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/11/26 15:14
 * @Version 1.0
 */
public class AsscDemo {

    public static void main(String[] args) throws IOException {
        AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
        server.bind(new InetSocketAddress(8080));

        System.out.println("AIO 服务器启动在端口 8080,等待连接...");

        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel client, Void attachment) {
                server.accept(null, this);

                try {
                    System.out.println("客户端上线: " + client.getRemoteAddress());
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    startRead(client, buffer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                System.err.println("Accept 失败");
                exc.printStackTrace();
            }
        });

        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 封装读取逻辑
     */
    private static void startRead(AsynchronousSocketChannel client, ByteBuffer buffer) {
        client.read(buffer, client, new CompletionHandler<Integer, AsynchronousSocketChannel>() {
            @Override
            public void completed(Integer result, AsynchronousSocketChannel channel) {
                if (result == -1) {
                    try {
                        System.out.println("客户端下线: " + channel.getRemoteAddress());
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    return;
                }

                buffer.flip();
                String msg = new String(buffer.array(), 0, buffer.limit(), StandardCharsets.UTF_8).trim();
                System.out.println("收到消息: " + msg);
                String response = "Server Echo: " + msg;
                ByteBuffer writeBuffer = ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8));
                startWrite(channel, writeBuffer, buffer);
            }

            @Override
            public void failed(Throwable exc, AsynchronousSocketChannel channel) {
                System.err.println("读取异常,关闭连接");
                try { channel.close(); } catch (IOException e) { }
            }
        });
    }

    /**
     * 封装写入逻辑
     */
    private static void startWrite(AsynchronousSocketChannel client, ByteBuffer writeBuffer, ByteBuffer readBuffer) {
        client.write(writeBuffer, client, new CompletionHandler<Integer, AsynchronousSocketChannel>() {
            @Override
            public void completed(Integer result, AsynchronousSocketChannel channel) {
                if (writeBuffer.hasRemaining()) {
                    client.write(writeBuffer, channel, this);
                } else {
                    readBuffer.clear();
                    startRead(channel, readBuffer);
                }
            }

            @Override
            public void failed(Throwable exc, AsynchronousSocketChannel channel) {
                System.err.println("写入异常,关闭连接");
                try { channel.close(); } catch (IOException e) { }
            }
        });
    }
}

看一下完整的执行流程:

 

2.3 CompletionHandler

这个类,上面我们其实已经用到了,是用来定义异步操作完成后的回调逻辑的。

CompletionHandler<V, A>中的V表示异步操作的结果类型。

对于accept,是AsynchronousSocketChannel。

对于read或者write,是Integer,表示读取/写入的字节数。

A是附件类型,这是我们调用异步方法accept或者read时传的第二个参数。可以是任何对象,用来在回调时传递上下文信息。

核心方法就两个,一个completed(V result, A attachment)、另一个failed(Throwable exc, A attachment)。

completed在异步操作成功完成时被调用。failed在异步操作失败时被调用。

2.4 异步文件通道

AsynchronousFileChannel用于文件的异步I/O。

通过一个示例看一下异步文件通道的使用:


package com.lazy.snail.day64;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
 * @ClassName AsyncFileDemo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/11/26 16:49
 * @Version 1.0
 */
public class AsyncFileDemo {
    public static void main(String[] args) throws Exception {
        String fileName = "C:\\Users\\lazysnail\\Desktop\\懒惰蜗牛.txt";

        System.out.println("[当前线程: " + Thread.currentThread().getName() + "] 准备调用 readWithCallback");

        readWithCallback(fileName);

        System.out.println("[当前线程: " + Thread.currentThread().getName() + "] 主线程准备睡觉,等待异步结果...");
        Thread.sleep(2000);
    }

    private static void readWithCallback(String fileName) throws IOException {
        AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
                Paths.get(fileName), StandardOpenOption.READ);
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        fileChannel.read(buffer, 0, null, new CompletionHandler<Integer, Void>() {
            @Override
            public void completed(Integer bytesRead, Void attachment) {
                System.out.println("-------------------------------------------");
                System.out.println("[当前线程: " + Thread.currentThread().getName() + "] 回调触发!读取完成!");

                if (bytesRead > 0) {
                    buffer.flip();
                    System.out.println("读取内容: " + new String(buffer.array(), 0, bytesRead));
                }
                try { fileChannel.close(); } catch (IOException e) {}
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("[当前线程: " + Thread.currentThread().getName() + "] 读取失败");
            }
        });

        System.out.println("[当前线程: " + Thread.currentThread().getName() + "] 已启动异步读取,read方法已返回!");
    }
}

运行结果:

 

执行时序图:

 

并行区域中,主线程和系统IO在同时工作。文件很小的时候,第13步可能会跑到第6步前面。

关键的第10步,数据是由操作系统直接拷贝到用户Buffer的,当回调线程启动的时候,数据已经在buffer里准备好了,可以直接用。这也是AIO和BIO最大的区别。

三、拓展

关于AIO大概就讲这么多了,这里讲一个Netty的小历史。

如果我们了解过网络框架Netty的历史,会发现他曾经支持过AIO,但在后续版本里移除了。

AIO感觉那么行,为啥会被“放弃”?

Java或者JVM不管是设计了还是统一了什么,怎么都还是应用层次的操作。

文件操作是在读写磁盘,网络操作是在读写网卡,这些都是特权操作,只有操作系统内核才有资格去碰硬件。

怎么读、读多快、能不能异步读,完全取决于操作系统提供了什么接口。

了解了这个概念,就能扯出Windows和Linux关于IO的差异了。

Windows中有一个内核机制叫IOCP,采用的就是Proactor模式。数据拷贝的工作可以由操作系统内核直接完成。

跟Java中的AIO异曲同工,所以Java能够在Windows上实现真异步。

而Linux在很长一段时间里,没有异步网络I/O支持。他用的是Epoll,是Reactor模式,也就是多路复用。

Java在Linux上为了实现AIO的效果,必须在后台搞一个线程池,当Epoll通知可读时,Java的后台线程去执行读取,读完了再假装通知我们:我帮你读完了,所以算异步哦。

底层本质还是非阻塞IO。

Netty的开发者在Linux搞了半天的AIO,发现效果不好,还不如直接优化NIO。

也就有了AIO被Netty放弃的小故事。

不过Linux已经在Kernel 5.1中引入了属于他的真异步IO,io_uring。

但是就算是9月份发布的Java 25在Linux上的实现也还是Epoll。

Netty倒是提供了插件包使用io_uring。

结语

截止本文,关于BIO、NIO、AIO我们基本上都过了一遍。

最后,我们用一张表来理一下三者:

特性

BIO (Blocking I/O)

NIO (Non-Blocking I/O)

AIO (Asynchronous I/O)

IO 模型

同步阻塞

同步非阻塞 (多路复用)

异步非阻塞

编程难度

简单

难 (Selector, Buffer)

难 (回调机制, 复杂流控)

可靠性

差 (线程数限制)

核心组件

Stream (流)

Channel, Buffer, Selector

Channel, CompletionHandler

适用场景

连接数少且固定的架构

连接数多且连接较短(轻操作)(如: 聊天室, 弹幕)

连接数多且连接较长(重操作)(如: 相册服务器, 文件传输)

谁负责数据拷贝

用户线程

用户线程

操作系统

下一篇预告

待定

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐