AIO、BIO 和 NIO 的区别?
结论:三者的本质区别是**"谁在等数据"**。BIO(blocking IO)同步阻塞,一个连接占一个线程,读不到数据线程就挂起;NIO(non-blocking IO)同步非阻塞,用 Selector 做多路复用(multiplexing),一个线程轮询管理成千上万个连接,就绪后再处理;AIO(asynchronous IO,也称 NIO.2)异步非阻塞,发起读写后立即返回,由操作系统完成后回调 CompletionHandler,应用线程全程不等待。
1. 三种模型的线程视图
BIO: client1 ──→ thread1(阻塞在 read)
client2 ──→ thread2(阻塞在 read)
client3 ──→ thread3(阻塞在 read) 连接数 ↑ ⇒ 线程数 ↑
NIO: client1 ─┐
client2 ─┼──→ Selector(轮询就绪事件)──→ 单线程分发 ──→ 业务线程池
client3 ─┘ 一个线程管 N 个连接
AIO: client ──→ 发起 read 并注册 CompletionHandler ──→ 立即返回
内核完成读 ──→ 回调线程执行 completed()
2. 逐项对比
| 维度 | BIO | NIO | AIO |
|---|---|---|---|
| IO 模型 | 同步阻塞 | 同步非阻塞 + 多路复用 | 异步非阻塞 |
| 核心 API | InputStream/OutputStream、Socket | Channel、Buffer、Selector | AsynchronousSocketChannel、CompletionHandler |
| 线程模型 | 1 连接 1 线程 | 1 个 selector 线程 + 少量 worker | 回调线程池 |
| 编程复杂度 | 低 | 高(半包、粘包、状态机) | 中(回调嵌套) |
| 吞吐能力 | 连接数少时够用 | 高并发首选 | 理论上更高,生态弱 |
| 典型应用 | 传统 ServerSocket、JDBC | Netty、Redis 客户端、Tomcat NIO | 少量文件/网络异步场景 |
3. 代码:NIO 的服务端骨架
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class NioServer {
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false); // 必须非阻塞才能注册
server.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer buf = ByteBuffer.allocate(1024);
while (true) {
selector.select(); // 阻塞直到有就绪事件
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove(); // 必须移除,否则重复处理
if (key.isAcceptable()) {
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
buf.clear();
int n = client.read(buf); // 非阻塞:可能读到 0 字节
if (n == -1) { client.close(); continue; }
buf.flip(); // 写模式切读模式
// 业务处理:注意 TCP 是字节流,要自己处理半包
}
}
}
}
}
对应的 BIO 版本简单得多,但每个连接都要 new Thread 或交给线程池,连接数一多线程就被耗尽。
4. 怎么选
| 场景 | 推荐 | 理由 |
|---|---|---|
| 连接数少(几十个)、逻辑简单 | BIO + 线程池 | 代码直观,排查容易 |
| 高并发长连接(IM、网关、推送) | NIO(一般直接用 Netty) | 少量线程支撑大量连接 |
| 大量小文件异步读写 | AIO / CompletableFuture | 避免线程阻塞在 IO |
| 内网 RPC、低延迟 | NIO + 池化 | 复用连接、减少握手 |
| 传统 Web 应用 | Tomcat NIO connector | 容器已封装好多路复用 |
5. 常见追问
| 追问 | 回答要点 |
|---|---|
| 同步/异步、阻塞/非阻塞怎么区分? | 阻塞指调用是否立即返回;同步指结果是否由调用方主动获取。NIO 是同步非阻塞(要自己 select 再读),AIO 是异步(内核通知) |
| 为什么 Netty 不用 AIO? | Linux 上 JDK 的 AIO 实现成熟度与性能都不如 epoll 多路复用,且调试困难 |
| 多路复用底层是什么? | Linux epoll、macOS kqueue、Windows IOCP;JDK 通过 SelectorProvider 适配 |
| NIO 就一定会快吗? | 不一定,连接少时多路复用反而多一次事件分发;它解决的是"连接数"而非"单连接延迟" |
| 为什么 NIO 要处理半包/粘包? | TCP 是字节流,没有消息边界,需要长度字段或分隔符切分 |
| BIO 的"伪异步"是什么? | 用线程池限制线程数、请求排队,避免线程无限增长,但阻塞本质未变 |
小结
BIO 用线程换简单,NIO 用事件换并发,AIO 用回调换等待。工程上几乎不会手写 NIO,而是用 Netty;面试要讲清 "Selector 多路复用 + 非阻塞 Channel" 这条主线,以及 BIO 在高连接数下线程耗尽的必然性。