核心概念:I/O 模型
在深入代码之前,我们先理解几个核心概念:

- 阻塞:当一个线程发起一个 I/O 操作(如读取数据),如果数据还没准备好,这个线程就会一直等待,直到数据准备好或发生超时,在此期间,线程被“阻塞”,无法执行其他任何任务。
- 非阻塞:当一个线程发起一个 I/O 操作,如果数据还没准备好,它会立即得到一个“未就绪”的返回,然后线程可以立即去做其他事情,过一段时间再来检查数据是否准备好。
- 同步:由用户线程自己去执行 I/O 操作(比如通过循环调用
read方法来检查数据是否就绪),数据在用户线程和内核之间进行拷贝。 - 异步:用户线程发起 I/O 操作后,就可以去做其他事情了,当 I/O 操作完成(数据已准备好并拷贝到用户空间)后,操作系统会通知用户线程(通过回调或事件),整个过程由操作系统内核完成,用户线程无需主动干预。
基于这些概念,我们可以组合出四种 I/O 模型:
- 同步阻塞 I/O (BIO)
- 同步非阻塞 I/O (NIO 的一种实现方式)
- I/O 多路复用 (NIO 的核心)
- 异步 I/O (AIO)
BIO (Blocking I/O) - 阻塞式 I/O
BIO 是 Java 最早提供的 I/O 模型,也是最简单、最容易理解的模型。
工作原理
- 服务端:启动一个
ServerSocket,在一个固定端口进行监听。 - 客户端:发起连接请求。
- 服务端处理:
- 服务端通过
ServerSocket.accept()方法来等待客户端连接。 accept()是一个阻塞方法,如果没有客户端连接,服务端线程会一直阻塞无法处理其他任何连接。- 当一个客户端连接成功,
accept()返回一个Socket对象,代表与这个客户端的连接通道。
- 服务端通过
- 数据读写:
- 服务端会为这个新的
Socket创建一个新的线程,在这个线程中进行数据的读写操作(InputStream.read()/OutputStream.write())。 read()和write()方法也都是阻塞方法,如果客户端没有发送数据,读线程会一直阻塞;如果发送缓冲区满了,写线程也会阻塞。
- 服务端会为这个新的
- 线程销毁:当通信结束,这个处理线程随之销毁。
代码示例 (BIO)
// 服务端
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Server started, listening on port 8080...");
while (true) {
// 1. 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("Client connected: " + clientSocket.getInetAddress().getHostAddress());
// 2. 为每个客户端创建一个新线程处理
new Thread(() -> {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String inputLine;
// 3. 阻塞读取客户端数据
while ((inputLine = in.readLine()) != null) {
System.out.println("Received from client: " + inputLine);
out.println("Server response: " + inputLine);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
优缺点
- 优点:
模型简单,编码直观,易于理解。
- 缺点:
- 性能差:每个连接都需要一个独立的线程,如果连接数非常多(比如成千上万),线程数会急剧膨胀,导致服务器资源(内存、CPU)耗尽,发生线程切换开销巨大等问题。
- 可靠性差:一个线程的阻塞(如长时间不发送数据)会影响整个服务,甚至可能导致宕机。
- 可扩展性差:无法应对高并发场景。
适用场景
- 连接数较少且固定的场景,如一些简单的内部应用、教学示例。
- 对性能要求不高的传统应用。
NIO (New I/O) - 非阻塞 I/O / I/O 多路复用
为了解决 BIO 的高并发问题,Java 1.4 引入了 NIO,NIO 的核心思想是用一个线程来管理多个连接,通过轮询的方式检查哪些连接已经准备好可以进行 I/O 操作。

核心组件
NIO 主要由三个核心部分组成:
- Channel (通道):类似传统的
Stream,但双向的,既可以读也可以写。SocketChannel和ServerSocketChannel是网络编程中最重要的两种通道。 - Buffer (缓冲区):数据被读取到一个
Buffer中,或者从Buffer中写入。Buffer是一块连续的内存区域,读写操作都是通过Buffer进行的,NIO 的读写都是缓冲区操作,而不是流操作。 - Selector (选择器):NIO 的“魔法”所在,一个
Selector可以同时监控多个Channel的 I/O 状态(如连接、读、写),当某个Channel有“就绪”的 I/O 事件时,Selector会将其“选择”出来,然后由我们进行处理。
工作原理
- 服务端:创建一个
ServerSocketChannel,并将其设置为非阻塞模式。 - 绑定 Selector:将这个
ServerSocketChannel注册到Selector上,并指定我们关心的事件(SelectionKey.OP_ACCEPT,即“接受连接”)。 - 轮询:在一个单独的线程中,启动一个无限循环,调用
Selector.select()方法。select()也是一个阻塞方法,但它不是阻塞在某个连接上,而是阻塞在Selector上,它会一直等待,直到至少有一个注册的Channel发生了我们关心的事件。
- 处理就绪事件:当
select()返回后,我们通过Selector.selectedKeys()获取所有“就绪”的Channel集合。 - 分发处理:遍历这个集合,根据事件的类型(
OP_ACCEPT,OP_READ,OP_WRITE)进行相应的处理:- 如果是
OP_ACCEPT,说明有新的客户端连接,接受连接,并得到一个SocketChannel。 - 将这个新的
SocketChannel也设置为非阻塞模式,并注册到Selector上,这次我们关心的是OP_READ事件。 - 如果是
OP_READ,说明某个SocketChannel有数据可读,创建一个Buffer,从Channel中读取数据到Buffer中。
- 如果是
代码示例 (NIO)
// 服务端
public class NioServer {
public static void main(String[] args) throws IOException {
// 1. 创建Selector和ServerSocketChannel
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
// 2. 将ServerSocketChannel注册到Selector,关心OP_ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Server started, listening on port 8080...");
while (true) {
// 3. 阻塞等待至少一个通道就绪
selector.select();
// 4. 获取所有就绪的SelectionKey
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove(); // 手动移除,防止重复处理
if (key.isAcceptable()) {
// 5. 处理连接就绪事件
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
// 将新的SocketChannel注册到Selector,关心OP_READ事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Client connected: " + socketChannel.getRemoteAddress());
}
if (key.isReadable()) {
// 6. 处理读就绪事件
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
key.cancel();
socketChannel.close();
System.out.println("Client disconnected.");
continue;
}
buffer.flip();
System.out.println("Received from client: " + new String(buffer.array(), 0, bytesRead));
// 简单回写
String response = "Server response: " + new String(buffer.array(), 0, bytesRead);
socketChannel.write(ByteBuffer.wrap(response.getBytes()));
}
}
}
}
}
优缺点
- 优点:
- 高并发:一个线程可以管理成百上千个连接,极大地减少了线程数和上下文切换的开销。
- 可扩展性好:非常适合处理高并发连接。
- 缺点:
- 编程复杂:需要理解
Channel,Buffer,Selector等概念,代码比 BIO 复杂。 - “伪”异步:虽然一个线程可以处理多个连接,但数据的读写仍然是同步的,当一个
Channel的数据读写时间很长时,会影响到其他Channel的处理(因为是在同一个线程的循环中)。
- 编程复杂:需要理解
适用场景
- 高并发服务器,如 Web 服务器、RPC 框架、聊天室等。
- 需要处理大量连接,但每个连接的 I/O 操作相对较短的场景。
AIO (Asynchronous I/O) - 异步 I/O
AIO 是 Java 7 中引入的 I/O 模型,是真正的“异步非阻塞” I/O,它将 I/O 操作完全交由操作系统内核完成,应用程序无需主动轮询,只需在 I/O 操作完成后通过回调函数或 Future 对象获取结果。
工作原理
- 服务端:创建一个
AsynchronousServerSocketChannel,并绑定端口。 - 发起异步操作:调用
AsynchronousServerSocketChannel.accept()方法,并传入一个CompletionHandler(回调处理器)。accept()方法是非阻塞的,它会立即返回,主线程可以继续执行其他任务。
- 内核处理:操作系统在后台(由专门的 I/O 线程池)等待并处理客户端的连接请求。
- 回调通知:当客户端连接成功,操作系统会自动调用我们之前传入的
CompletionHandler的completed方法,并将代表连接的AsynchronousSocketChannel对象作为参数传递过来。 - 读写操作:在
completed方法中,我们同样可以发起异步的读写操作(如read(ByteBuffer buffer, CompletionHandler)),同样传入回调处理器,形成一个异步链式调用。
代码示例 (AIO)
// 服务端
public class AioServer {
public static void main(String[] args) throws IOException {
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
System.out.println("AIO Server started, listening on port 8080...");
// 1. 开始接受连接,传入一个CompletionHandler
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
// 2. 如果成功,继续接受下一个连接(递归调用)
serverChannel.accept(null, this);
System.out.println("Client connected: " + clientChannel.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. 异步读取数据
clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer attachment) {
if (bytesRead == -1) {
try {
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
attachment.flip();
System.out.println("Received from client: " + new String(attachment.array(), 0, bytesRead));
String response = "Server response: " + new String(attachment.array(), 0, bytesRead);
// 4. 异步写回数据
clientChannel.write(ByteBuffer.wrap(response.getBytes()), null, new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
// 写完成,可以继续读或其他操作
// 这里为了简化,我们不继续读了
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
// 主线程不能退出,否则程序会终止
// 在真实应用中,可能需要一个while(true)来保持主线程运行
// 或者使用CountDownLatch等工具
System.out.println("Server is running...");
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
优缺点
- 优点:
- 真正的异步:性能最高,I/O 操作完全由内核和 I/O 线程处理,业务线程(主线程)完全不被阻塞。
- 编程模型更符合“异步”思维:通过回调或
Future处理结果,逻辑清晰。
- 缺点:
- 编程复杂:回调地狱(Callback Hell),代码逻辑难以追踪和维护。
- 系统开销大:依赖操作系统的 AIO 实现,在 Windows 上表现良好,但在 Linux 上,其性能提升并不总是明显,且可能比 NIO 更消耗资源。
- 生态不完善:相比 NIO,AIO 的使用案例和社区支持较少。
适用场景
- 超高并发、I/O 密集型的应用,如网络存储、高性能消息中间件。
- 对延迟要求极高的场景。
总结与对比
| 特性 | BIO (Blocking I/O) | NIO (New I/O) | AIO (Asynchronous I/O) |
|---|---|---|---|
| I/O 模型 | 同步阻塞 | 同步非阻塞 / I/O 多路复用 | 异步非阻塞 |
| 核心思想 | 一个连接一个线程 | 一个线程管理多个连接 | I/O 操作由内核完成,线程回调 |
| 线程模型 | 每个连接一个新线程 | 单线程或少量线程管理所有连接 | 业务线程与 I/O 线程分离 |
| 阻塞点 | accept(), read(), write() |
select() (阻塞在 Selector 上) |
无阻塞,通过回调通知 |
| 性能 | 低,无法高并发 | 高,可处理大量连接 | 极高,但依赖系统和实现 |
| 编程复杂度 | 简单 | 复杂 | 复杂,易出现回调地狱 |
| JDK 版本 | 0+ | 4+ | 7+ |
| 适用场景 | 低并发、连接数少 | 高并发、通用场景 | 超高并发、延迟敏感型 |
如何选择?
- 新手或简单应用:从 BIO 开始,简单直观。
- 绝大多数高并发应用:NIO 是目前 Java 网络编程的首选和事实标准,像 Netty、Vert.x 等优秀的网络框架都是基于 NIO 实现的,它在性能、复杂度和适用性之间取得了最好的平衡。
- 极限性能挑战:如果你的应用是顶级的 I/O 密集型服务,并且你对 AIO 的复杂性有驾驭能力,可以尝试 AIO,但通常情况下,一个精心设计的 NIO 方案已经足够强大。

