Java IO 模型深度解析:BIO、NIO 与 AIO
前言
Java 中的 IO 模型是每位后端开发者必须掌握的核心知识。从传统的 BIO(Blocking I/O) 到 NIO(Non-blocking I/O),再到 AIO(Asynchronous I/O),Java 的 I/O 模型经历了三次重要演进。本文将深入剖析这三种模型的设计思想、实现原理和适用场景,并通过完整代码示例帮助你真正理解它们的区别。
一、BIO — 阻塞 I/O 模型
1.1 什么是 BIO
BIO(Blocking I/O)即同步阻塞 I/O 模型,是 Java 最早的 I/O 方式。在 java.net 包中,ServerSocket 和 Socket 是核心类。服务端每接受一个客户端连接,就需要一个线程去处理该连接上的读写操作。
核心特点:
- 一个连接对应一个线程
- 线程在
accept()、read()、write()等操作时会阻塞 - 数据未就绪时线程挂起,不占用 CPU 但占用系统资源
1.2 BIO 的工作机制
客户端1 ──→ 服务端主线程 ──→ 线程1(处理客户端1)
客户端2 ──→ ──→ 线程2(处理客户端2)
客户端3 ──→ ──→ 线程3(处理客户端3)
主线程负责 accept 新的连接,每 accept 一个连接就启动一个新线程处理该连接的读写。
1.3 传统 BIO 编程示例
服务端代码
package com.example.bio;
import java.io.*;
import java.net.*;
/**
* 传统 BIO 服务端
* 每个客户端连接对应一个独立线程
*/
public class BioServer {
private static final int PORT = 8080;
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("BIO Server 启动,监听端口:" + PORT);
while (true) {
// ★ 阻塞点1:accept() 阻塞,直到有客户端连接
Socket socket = serverSocket.accept();
System.out.println("接收到客户端连接:" + socket.getRemoteSocketAddress());
// 每个连接启动一个新线程处理
new Thread(new ClientHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
static class ClientHandler implements Runnable {
private final Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(
socket.getOutputStream(), true)) {
String message;
// ★ 阻塞点2:readLine() 阻塞,直到读取到一行数据
while ((message = reader.readLine()) != null) {
System.out.println("收到消息:" + message);
writer.println("服务端回复:" + message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端代码
package com.example.bio;
import java.io.*;
import java.net.*;
/**
* BIO 客户端
*/
public class BioClient {
private static final String HOST = "127.0.0.1";
private static final int PORT = 8080;
public static void main(String[] args) {
try (Socket socket = new Socket(HOST, PORT);
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(
socket.getOutputStream(), true);
BufferedReader consoleReader = new BufferedReader(
new InputStreamReader(System.in))) {
System.out.println("已连接到服务器,请输入消息:");
String input;
while ((input = consoleReader.readLine()) != null) {
writer.println(input);
String response = reader.readLine();
System.out.println("服务器响应:" + response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.4 BIO 的痛点
| 问题 | 说明 |
|---|---|
| 线程开销大 | 每个连接一个线程,线程创建销毁和上下文切换成本高 |
| 连接数受限 | 系统线程数量有限,当连接数达到数千时性能急剧下降 |
| 资源浪费 | 大量线程在等待 I/O 时阻塞,仅占内存却不做实际工作 |
| 伸缩性差 | 无法有效支撑高并发场景(如 C10K 问题) |
伪代码示意 BIO 的阻塞本质:
// 用户态视角
while (true) {
// 线程在此处阻塞 —— 操作系统将线程标记为 WAITING
// 移出调度队列,直到有数据到达才唤醒
byte[] data = socket.read();
process(data);
}
在操作系统层面,阻塞 I/O 会将线程从运行队列移入等待队列,数据到达后再通过中断或 select/epoll 唤醒线程。这个过程涉及 用户态 ↔ 内核态 的切换。
二、NIO — 非阻塞 I/O 模型
2.1 什么是 NIO
NIO(Non-blocking I/O / New I/O)是 JDK 1.4 引入的全新 I/O 库(java.nio 包),提供了面向缓冲区(Buffer) 和通道(Channel) 的 I/O 方式,并引入了多路复用器(Selector) 实现单线程管理多个连接。
核心思想: 一个线程可以管理成千上万个连接,通过事件驱动机制只在数据真正就绪时才进行实际读写操作。
2.2 NIO 三大核心组件
┌─────────────────────────────────────────────┐
│ NIO 架构 │
│ │
│ 线程 ──→ Selector │
│ │ │ │ │
│ SelectionKey SelectionKey SelectionKey │
│ │ │ │ │
│ ChannelA ChannelB ChannelC │
│ │ │ │ │
│ Buffer Buffer Buffer │
│ │ │ │ │
│ Data Data Data │
└─────────────────────────────────────────────┘
2.2.1 Channel(通道)
Channel 类似传统 IO 中的流(Stream),但有以下区别:
| 传统 Stream | Channel |
|---|---|
| 单向(InputStream / OutputStream) | 双向(可读可写) |
| 基于字节/字符流 | 基于缓冲区(Buffer) |
| 阻塞式 | 可配置为非阻塞模式 |
| 不支持多路复用 | 可与 Selector 配合使用 |
常用 Channel 类型:
// 文件通道(阻塞模式,不支持 Selector)
FileChannel fileChannel = FileChannel.open(Paths.get("test.txt"), StandardOpenOption.READ);
// 服务端套接字通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 客户端套接字通道
SocketChannel socketChannel = SocketChannel.open();
// UDP 数据报通道
DatagramChannel datagramChannel = DatagramChannel.open();
2.2.2 Buffer(缓冲区)
Buffer 是 NIO 中数据读写的载体。所有数据都通过 Buffer 进行读写,Channel 从 Buffer 读或向 Buffer 写。
Buffer 的核心属性:
public abstract class Buffer {
// 容量 —— 缓冲区最大大小,一旦创建不可改变
private int capacity;
// 读写位置 —— 下一个要读写的位置索引
private int position;
// 界限 —— 缓冲区中有效数据的末尾位置
private int limit;
// 标记 —— 用于记录 position 位置,配合 reset() 使用
private int mark;
}
Buffer 的四个状态:
初始状态 (allocate):
position=0, limit=capacity, capacity=10
[ 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 ]
↑
pos, limit
写入 5 个字节后:
position=5, limit=capacity=10
[ H | e | l | l | o | | | | | ]
↑
pos limit
调用 flip() 切换为读模式:
position=0, limit=5
[ H | e | l | l | o | | | | | ]
↑ ↑
pos limit
读取 3 个字节后:
position=3, limit=5
[ H | e | l | l | o | | | | | ]
↑ ↑
pos limit
Buffer API 示例:
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class BufferExample {
public static void main(String[] args) {
// 1. 分配缓冲区(堆内存)
ByteBuffer buffer = ByteBuffer.allocate(10);
printBuffer("初始状态", buffer); // pos=0, lim=10, cap=10
// 2. 写入数据
buffer.put((byte) 'H');
buffer.put((byte) 'e');
buffer.put((byte) 'l');
buffer.put((byte) 'l');
buffer.put((byte) 'o');
printBuffer("写入5字节后", buffer); // pos=5, lim=10, cap=10
// 3. 翻转 —— 从写模式切换到读模式
buffer.flip();
printBuffer("flip()后", buffer); // pos=0, lim=5, cap=10
// 4. 读取数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
printBuffer("读取后", buffer); // pos=5, lim=5, cap=10
// 5. 重绕 —— 重新从头读
buffer.rewind();
printBuffer("rewind()后", buffer); // pos=0, lim=5, cap=10
// 6. clear() —— 清空缓冲区(只是重置位置,不清除数据)
buffer.clear();
printBuffer("clear()后", buffer); // pos=0, lim=10, cap=10
// 7. compact() —— 只清除已读数据,未读数据保留并移动至开头
buffer.put("Hello World!".getBytes(StandardCharsets.UTF_8));
buffer.flip();
buffer.get(); // 读走 'H'
buffer.get(); // 读走 'e'
buffer.compact();
printBuffer("compact()后", buffer); // "llo World!" 保留
}
private static void printBuffer(String msg, ByteBuffer buf) {
System.out.printf("%s → position=%d, limit=%d, capacity=%d%n",
msg, buf.position(), buf.limit(), buf.capacity());
}
}
Buffer 类型一览:
| Buffer 类型 | 描述 |
|---|---|
| ByteBuffer | 最常用,可存储字节 |
| CharBuffer | 存储字符 |
| ShortBuffer | 存储 short 类型 |
| IntBuffer | 存储 int 类型 |
| LongBuffer | 存储 long 类型 |
| FloatBuffer | 存储 float 类型 |
| DoubleBuffer | 存储 double 类型 |
| MappedByteBuffer | 内存映射文件缓冲区(FileChannel.map()) |
2.2.3 Selector(选择器 / 多路复用器)
Selector 是 NIO 的核心。它允许一个线程同时监控多个 Channel 的 I/O 事件(连接、读取、写入等),实现单线程管理多连接。
Selector 工作原理:
Selector selector = Selector.open();
channel.configureBlocking(false); // 必须配置为非阻塞模式
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
可注册的事件类型:
| 事件 | SelectionKey 常量 | 触发条件 |
|---|---|---|
| 连接就绪 | OP_CONNECT | 客户端连接成功 |
| 接受连接 | OP_ACCEPT | 服务端接收到新连接 |
| 可读 | OP_READ | 通道中有数据可读 |
| 可写 | OP_WRITE | 通道可以写入数据 |
SelectionKey 对象:
// 注册时返回的 SelectionKey 包含了通道和选择器的关联信息
SelectionKey key = channel.register(selector, ops);
// 常用方法
key.channel(); // 获取关联的通道
key.selector(); // 获取关联的选择器
key.interestOps(); // 感兴趣的事件集合
key.readyOps(); // 已就绪的事件集合
key.attachment(); // 获取附件对象(可用来存储会话状态)
// 判断事件类型
key.isAcceptable(); // 有新的连接可以接受
key.isConnectable(); // 连接完成
key.isReadable(); // 有数据可读
key.isWritable(); // 可以写入数据
2.3 NIO 完整服务端示例
以下是一个基于 NIO 实现的 Echo 服务器,展示 Selector + Channel + Buffer 的完整工作流程:
package com.example.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* 基于 NIO 的 Echo 服务器
* 单线程管理多个客户端连接
*/
public class NioEchoServer {
private static final int PORT = 8080;
private static final int BUFFER_SIZE = 1024;
private Selector selector;
private ServerSocketChannel serverChannel;
private ByteBuffer readBuffer;
public void start() throws IOException {
// 1. 打开 Selector
selector = Selector.open();
// 2. 打开 ServerSocketChannel
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // ★ 必须非阻塞
serverChannel.bind(new InetSocketAddress(PORT));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Server 启动,监听端口:" + PORT);
// 3. 初始化读缓冲区
readBuffer = ByteBuffer.allocate(BUFFER_SIZE);
// 4. 事件循环
while (true) {
// ★ 核心方法:select() 阻塞,直到至少一个 Channel 有事件就绪
int readyCount = selector.select();
if (readyCount == 0) {
continue;
}
// 获取就绪的 SelectionKey 集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
try {
// 处理就绪事件
if (key.isAcceptable()) {
handleAccept(key);
} else if (key.isReadable()) {
handleRead(key);
} else if (key.isWritable()) {
handleWrite(key);
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();
key.channel().close();
}
// ★ 必须移除已处理的 key,否则下次 select() 会重复返回
keyIterator.remove();
}
}
}
/**
* 处理新连接:OP_ACCEPT
*/
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = ssc.accept();
clientChannel.configureBlocking(false);
// 为新连接注册 OP_READ 事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("新客户端连接:" + clientChannel.getRemoteAddress());
}
/**
* 处理可读事件:OP_READ
*/
private void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
// 清空缓冲区,准备读取新数据
readBuffer.clear();
int bytesRead = clientChannel.read(readBuffer);
if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("客户端断开:" + clientChannel.getRemoteAddress());
key.cancel();
clientChannel.close();
return;
}
// 切换为读模式,读取数据
readBuffer.flip();
byte[] data = new byte[readBuffer.remaining()];
readBuffer.get(data);
String message = new String(data, StandardCharsets.UTF_8);
System.out.println("收到消息 [" + clientChannel.getRemoteAddress() + "]: " + message);
// ★ 将回复数据附加到 key 上,同时注册 OP_WRITE 事件
byte[] responseBytes = ("服务端回复: " + message).getBytes(StandardCharsets.UTF_8);
ByteBuffer writeBuffer = ByteBuffer.wrap(responseBytes);
key.attach(writeBuffer);
// 修改感兴趣的事件为 OP_WRITE
key.interestOps(SelectionKey.OP_WRITE);
}
/**
* 处理可写事件:OP_WRITE
*/
private void handleWrite(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer writeBuffer = (ByteBuffer) key.attachment();
if (writeBuffer == null) {
key.interestOps(SelectionKey.OP_READ);
return;
}
// 将缓冲区数据写入通道
while (writeBuffer.hasRemaining()) {
clientChannel.write(writeBuffer);
}
// ★ 写入完成后切换回 OP_READ,等待下一条消息
key.interestOps(SelectionKey.OP_READ);
key.attach(null); // 清理附件
}
public static void main(String[] args) throws IOException {
new NioEchoServer().start();
}
}
2.4 NIO 客户端示例
package com.example.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
/**
* 基于 NIO 的客户端
*/
public class NioClient {
private static final String HOST = "127.0.0.1";
private static final int PORT = 8080;
private Selector selector;
private SocketChannel channel;
public void start() throws IOException {
selector = Selector.open();
channel = SocketChannel.open();
channel.configureBlocking(false);
// 连接服务器(非阻塞)
channel.connect(new InetSocketAddress(HOST, PORT));
channel.register(selector, SelectionKey.OP_CONNECT);
System.out.println("连接服务器 " + HOST + ":" + PORT);
// 读线程
new Thread(this::readLoop).start();
// 写 —— 从控制台读取输入并发送
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String input = scanner.nextLine();
if ("quit".equalsIgnoreCase(input)) {
break;
}
ByteBuffer buffer = ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8));
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
channel.close();
selector.close();
}
private void readLoop() {
try {
while (true) {
int readyCount = selector.select();
if (readyCount == 0) continue;
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isConnectable()) {
handleConnect(key);
} else if (key.isReadable()) {
handleRead(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleConnect(SelectionKey key) throws IOException {
SocketChannel sc = (SocketChannel) key.channel();
if (sc.finishConnect()) {
System.out.println("连接成功!");
sc.register(selector, SelectionKey.OP_READ);
}
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = sc.read(buffer);
if (bytesRead == -1) {
sc.close();
return;
}
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("服务器响应:" + new String(data, StandardCharsets.UTF_8));
}
public static void main(String[] args) throws IOException {
new NioClient().start();
}
}
2.5 NIO 关键方法详解
select() 方法
// 阻塞直到至少一个 Channel 就绪
int select();
// 阻塞最多 timeout 毫秒
int select(long timeout);
// 非阻塞,立即返回就绪 Channel 数量
int selectNow();
关于 OP_WRITE 事件
这是一个容易被误解的事件。大多数情况下通道都是可写的,如果注册了 OP_WRITE 事件,select() 几乎会立即返回。正确的用法是:仅在真正需要写入数据时才注册 OP_WRITE,写入完成后立即改回 OP_READ。
// ❌ 错误:一开始就注册 WRITE
channel.register(selector, SelectionKey.OP_WRITE);
// ✅ 正确:有数据要发送时才注册,发送完后切回 READ
key.interestOps(SelectionKey.OP_WRITE);
// ... 发送数据 ...
key.interestOps(SelectionKey.OP_READ);
三、BIO 与 NIO 全面对比
3.1 对比表格
| 维度 | BIO | NIO |
|---|---|---|
| IO 模型 | 同步阻塞 | 同步非阻塞 |
| 编程模型 | 一个连接一个线程 | 一个线程管理多个连接(多路复用) |
| 核心类 | ServerSocket, Socket | ServerSocketChannel, SocketChannel, Selector |
| 数据载体 | 流(Stream,单向) | 缓冲区(Buffer,双向) |
| 资源消耗 | 线程数随连接数线性增长,高 | 线程数恒定(通常 1~CPU核心数),低 |
| 并发能力 | 连接数几百左右达到瓶颈 | 支持数千到数万连接(C10K 级别) |
| 适用场景 | 低并发、短连接、固定连接数 | 高并发、长连接、连接数波动大 |
| 编程复杂度 | 低,直观 | 较高,需处理粘包/半包、状态管理 |
| JDK 版本 | JDK 1.0+ | JDK 1.4+ |
| 操作系统支持 | 所有系统 | 需系统支持非阻塞 IO |
3.2 线程模型对比
BIO 线程模型(连接数 = 线程数):
┌──────────┐ ┌──────────┐
│ Thread 1 │◄──►│ Client 1 │
├──────────┤ ├──────────┤
│ Thread 2 │◄──►│ Client 2 │
├──────────┤ ├──────────┤
│ Thread 3 │◄──►│ Client 3 │
├──────────┤ ├──────────┤
│ ... │ │ ... │
└──────────┘ └──────────┘
NIO 线程模型(单线程管理 N 连接):
┌──────────┐
│ Thread │◄── Selector ──► Channel 1 (Client 1)
│ │ ├── Channel 2 (Client 2)
│ │ ├── Channel 3 (Client 3)
│ │ ├── ...
│ │ └── Channel N (Client N)
└──────────┘
3.3 消息读取方式对比
// BIO:面向流,直接读取
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf); // 阻塞直到有数据
// NIO:面向缓冲区,通过 Channel 读取
SocketChannel channel = socket.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer); // 可能立即返回 0
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
3.4 性能对比参考
| 连接数 | BIO 响应时间 | NIO 响应时间 | 说明 |
|---|---|---|---|
| 100 | 5ms | 3ms | 低负载下差异不大 |
| 1,000 | 120ms | 8ms | BIO 开始出现明显线程竞争 |
| 5,000 | 超时/拒绝连接 | 15ms | BIO 线程数达到系统瓶颈 |
| 10,000 | 无法运行 | 25ms | NIO 依然稳定 |
注:以上数据为实验室环境下的参考值,实际表现取决于硬件、操作系统和 JVM 参数。
四、AIO — 异步 I/O 模型
4.1 什么是 AIO
AIO(Asynchronous I/O)即异步非阻塞 I/O,JDK 1.7 引入(NIO.2)。与 NIO 的区别在于:
- NIO(同步非阻塞): 用户线程通过 Selector 轮询数据是否就绪,就绪后自己读取
- AIO(异步非阻塞): 用户线程发出 I/O 请求后直接返回,数据就绪后由操作系统回调通知
4.2 AIO 的工作方式
// AIO 核心类
AsynchronousServerSocketChannel // 服务端异步通道
AsynchronousSocketChannel // 客户端异步通道
AsynchronousFileChannel // 文件异步通道
CompletionHandler // 回调处理器
4.3 AIO 服务端示例
package com.example.aio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* AIO 服务端示例
*/
public class AioServer {
private static final int PORT = 8080;
public void start() throws IOException {
AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(PORT));
System.out.println("AIO Server 启动,监听端口:" + PORT);
// 方式一:使用 Future(轮询方式)
// serverChannel.accept();
// 方式二:使用 CompletionHandler(推荐)
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
// 接受下一个连接(实现链式调用)
serverChannel.accept(null, this);
System.out.println("新客户端连接:" + getRemoteAddress(clientChannel));
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 异步读取
clientChannel.read(buffer, buffer, new ReadCompletionHandler(clientChannel));
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("接受连接失败:" + exc.getMessage());
serverChannel.accept(null, this);
}
});
System.out.println("服务器已启动,按任意键停止...");
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取操作的 CompletionHandler
*/
private static class ReadCompletionHandler
implements CompletionHandler<Integer, ByteBuffer> {
private final AsynchronousSocketChannel clientChannel;
public ReadCompletionHandler(AsynchronousSocketChannel clientChannel) {
this.clientChannel = clientChannel;
}
@Override
public void completed(Integer bytesRead, ByteBuffer buffer) {
if (bytesRead == -1) {
closeChannel();
return;
}
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data, StandardCharsets.UTF_8);
System.out.println("收到消息: " + message);
// 准备回复数据
byte[] response = ("回复: " + message).getBytes(StandardCharsets.UTF_8);
ByteBuffer writeBuffer = ByteBuffer.wrap(response);
// 异步写入
clientChannel.write(writeBuffer, writeBuffer,
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
if (attachment.hasRemaining()) {
clientChannel.write(attachment, attachment, this);
} else {
// 写入完成,继续读取下一条
ByteBuffer newBuffer = ByteBuffer.allocate(1024);
clientChannel.read(newBuffer, newBuffer,
new ReadCompletionHandler(clientChannel));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("写入失败:" + exc.getMessage());
closeChannel();
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
System.err.println("读取失败:" + exc.getMessage());
closeChannel();
}
private void closeChannel() {
try {
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static String getRemoteAddress(AsynchronousSocketChannel channel) {
try {
return channel.getRemoteAddress().toString();
} catch (IOException e) {
return "unknown";
}
}
public static void main(String[] args) throws IOException {
new AioServer().start();
}
}
4.4 AIO 的适用场景
| 场景 | 适用性 | 说明 |
|---|---|---|
| 文件 I/O | ✅ 非常适合 | 操作系统原生支持异步文件读写 |
| 高吞吐网络 | ⚠️ 谨慎选择 | Linux 下 AIO 实现不够成熟,底层仍用 epoll |
| 低延迟要求 | ❌ 不如 NIO | AIO 的回调处理有一定额外开销 |
| 简单编程模型 | ✅ 方便 | 回调方式比 NIO 的轮询更易理解 |
4.5 三种模型的演进关系
BIO (JDK 1.0)
│
├── 同步阻塞,一个连接一个线程
├── 编程简单,适合低并发
│
▼
NIO (JDK 1.4)
│
├── 同步非阻塞,多路复用
├── Channel + Buffer + Selector
├── 单线程管理数千连接
│
▼
AIO (JDK 1.7 / NIO.2)
│
├── 异步非阻塞,回调驱动
├── 真正解放线程
├── 编程模型更简洁
└── Linux 下底层仍依赖 epoll(非原生异步 I/O)
五、实际场景中的最佳实践
5.1 如何选择
连接数 < 500 且 短连接?
└── BIO(简单够用)
连接数 > 500 或 长连接?
└── NIO(推荐)
└── 考虑使用 Netty 等高性能 NIO 框架
大量文件 I/O 且 不频繁?
└── AIO(异步文件读写)
高吞吐网络服务器?
└── NIO(Netty)> AIO > BIO
5.2 常见 NIO 陷阱
// 陷阱1:忘记设置非阻塞
ServerSocketChannel channel = ServerSocketChannel.open();
// channel.configureBlocking(false); // 缺少这行会抛异常
channel.register(selector, SelectionKey.OP_ACCEPT); // ❌ IllegalBlockingModeException
// 陷阱2:忘记移除已处理的 SelectionKey
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) { // ❌ 没有 iterator.remove()
handle(key);
}
// 下次 select() 会再次返回同样的 key,导致无限循环
// 陷阱3:OP_WRITE 滥用
channel.register(selector, SelectionKey.OP_WRITE); // ❌ 几乎立即就绪
// 应仅在需要写数据时临时注册
// 陷阱4:缓冲区大小不当
ByteBuffer buffer = ByteBuffer.allocate(1); // ❌ 太小,频繁 I/O
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // ❌ 太大,浪费内存
// 通常 1024~8192 字节较为合理
5.3 生产级别的 NIO 框架
虽然直接使用 JDK NIO 能让你深入理解底层,但生产环境中更推荐使用封装好的框架:
| 框架 | 特点 |
|---|---|
| Netty | 最流行的 NIO 框架,Dubbo、RocketMQ 等都在用 |
| Vert.x | 反应式编程,支持多语言 |
| Grizzly | GlassFish 的 NIO 框架,成熟稳定 |
| JDK 自带 | 适合学习,或简单场景 |
Netty 版本的 Echo 服务端(对比参考):
// Netty 的代码量比原生 NIO 少 60% 以上
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new EchoServerHandler());
}
});
ChannelFuture f = b.bind(8080).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
六、总结
| 模型 | 类型 | 线程模型 | 并发能力 | 适用场景 |
|---|---|---|---|---|
| BIO | 同步阻塞 | 连接 : 线程 = 1 : 1 | 低(~500) | 连接数少、低并发、简单应用 |
| NIO | 同步非阻塞 | 连接 : 线程 = N : 1 | 高(数万) | 高并发、长连接、通用网络服务 |
| AIO | 异步非阻塞 | 线程 : 回调 : 数据 = 1 : 1 : N | 高 | 文件 I/O、对延迟不敏感的场景 |
一句话总结:
BIO 是「一个人端一碗饭」,NIO 是「一个人用传送带管理多碗饭」,AIO 是「饭好了叫你来吃」。
推荐学习路径
- 先掌握 BIO:理解传统 Socket 编程、阻塞的本质
- 再学习 NIO的三大核心:Buffer → Channel → Selector(按顺序)
- 动手实现一个简单的 NIO 服务器(反复调试理解事件驱动)
- 学习 Netty 框架:大量生产验证,帮你避开 NIO 的坑
- 拓展了解 AIO:知道存在即可,实际项目中用得较少
本文通过 Halo 博客发布,示例代码可在 GitHub 仓库 [链接] 查看完整源码。