Day64 | Java IO之AIO (NIO.2) 详解
本文介绍了Java中的异步I/O(AIO)技术,对比了BIO、NIO和AIO三种I/O模型的特点。AIO作为NIO.2引入的异步非阻塞I/O,通过回调机制实现真正的异步操作,无需轮询等待。文章详细讲解了AIO的核心类(AsynchronousServerSocketChannel、AsynchronousSocketChannel等)和使用方法,并通过代码示例展示了异步文件读取的实现。最后分析了A
在前两篇文章中,我们深入挖掘了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 |
|
适用场景 |
连接数少且固定的架构 |
连接数多且连接较短(轻操作)(如: 聊天室, 弹幕) |
连接数多且连接较长(重操作)(如: 相册服务器, 文件传输) |
|
谁负责数据拷贝 |
用户线程 |
用户线程 |
操作系统 |
下一篇预告
待定
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
更多推荐




所有评论(0)