基于 Java NIO 的非阻塞模式、BIO 阻塞模式
·
一、BIO(阻塞 I/O)的底层原理
BIO 基于操作系统的阻塞式 I/O 模型,其核心特点是线程会被内核阻塞直到 I/O 操作完成。
1. 底层流程(以读操作为例)
- 用户态发起读请求:Java 调用
InputStream.read()时,会触发系统调用(如 Linux 的read()),从用户态切换到内核态。 - 内核等待数据:内核需要等待数据从外部设备(如网卡、磁盘)加载到内核缓冲区(Kernel Buffer),这个过程中,用户线程会被挂起(阻塞),释放 CPU 资源。
- 数据拷贝:当数据到达内核缓冲区后,内核将数据从内核缓冲区拷贝到用户缓冲区(User Buffer)。
- 唤醒线程:拷贝完成后,内核唤醒阻塞的用户线程,线程从内核态切换回用户态,继续执行后续逻辑。
2. 线程阻塞的本质
- 阻塞过程中,线程处于
WAITING状态,不消耗 CPU,但会占用系统资源(如线程栈内存)。 - 每个连接对应一个线程,当并发量高时,线程数量激增,会导致:
- 线程上下文切换开销增大(CPU 频繁切换线程状态)。
- 内存占用过高(每个线程默认栈大小为 1MB 左右)。、
BIO 服务器端示例代码:
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("BIO 服务器启动,监听端口 8080...");
while (true) {
// 阻塞等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("新客户端连接:" + socket.getInetAddress());
// 为每个连接创建新线程处理
new Thread(() -> {
try {
handleClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
private static void handleClient(Socket socket) throws IOException {
// 读取客户端数据(阻塞操作)
byte[] buffer = new byte[1024];
int len = socket.getInputStream().read(buffer);
if (len > 0) {
System.out.println("收到客户端数据:" + new String(buffer, 0, len));
}
// 向客户端写数据(阻塞操作)
socket.getOutputStream().write("已收到数据".getBytes());
socket.close();
}
}
二、NIO(非阻塞 I/O)的底层原理
NIO 基于操作系统的非阻塞 I/O 模型和多路复用 I/O 模型,核心是通过Selector(选择器) 实现单线程管理多个 I/O 通道。
1. 非阻塞 I/O 的核心机制
- 非阻塞读 / 写:当调用
SocketChannel.read()时,如果内核缓冲区没有数据,会立即返回 0(而非阻塞),线程可以继续处理其他任务。 - 缓冲区(Buffer):数据必须通过 Buffer 交互,本质是一块用户态内存,减少用户态与内核态的切换次数(通过一次拷贝批量处理数据)。
2. 多路复用(Selector)的底层原理
Selector 是 NIO 的核心,其底层依赖操作系统的多路复用器(如 Linux 的 epoll、Windows 的 IOCP、macOS 的 kqueue)。以 Linux 的 epoll 为例:
- 注册事件:Java 将 Channel(如
SocketChannel)注册到 Selector 时,底层会调用epoll_ctl向内核的epoll实例注册事件(如连接、读、写)。 - 等待事件就绪:调用
selector.select()时,底层会调用epoll_wait,阻塞等待内核通知就绪的 I/O 事件(这是 NIO 中唯一可能阻塞的地方,但只阻塞一个线程)。 - 事件分发:当有 I/O 事件就绪(如数据到达),内核会将就绪事件列表返回给用户态,Selector 遍历事件列表,唤醒对应的 Channel 进行处理。
3. 流程对比(与 BIO 对比)
- BIO:一个线程处理一个连接,全程阻塞等待 I/O。
- NIO:一个线程通过 Selector 管理所有连接,仅在
select()时阻塞,数据就绪后才处理,大幅减少线程数量。
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NioServer {
public static void main(String[] args) throws IOException {
// 创建选择器
Selector selector = Selector.open();
// 创建服务器通道并绑定端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 注册到选择器,关注连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务器启动,监听端口 8080...");
while (true) {
// 阻塞等待就绪的通道(可以设置超时时间)
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
// 获取就绪通道的 SelectionKey 集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 处理连接事件
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受客户端连接(非阻塞)
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
// 注册到选择器,关注读事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("新客户端连接:" + socketChannel.getRemoteAddress());
}
// 处理读事件
else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据(非阻塞)
int len = socketChannel.read(buffer);
if (len > 0) {
buffer.flip();
System.out.println("收到客户端数据:" + new String(buffer.array(), 0, len));
// 响应客户端
buffer.clear();
buffer.put("已收到数据".getBytes());
buffer.flip();
socketChannel.write(buffer);
} else if (len == -1) {
// 客户端断开连接
socketChannel.close();
System.out.println("客户端断开连接");
}
}
// 移除已处理的 SelectionKey
iterator.remove();
}
}
}
}
三、BIO 与 NIO 底层原理对比
| 维度 | BIO 底层原理 | NIO 底层原理 |
|---|---|---|
| I/O 模型 | 阻塞式 I/O | 非阻塞 I/O + 多路复用 I/O |
| 线程与连接关系 | 一对一(一个线程绑定一个连接) | 一对多(一个线程管理多个连接) |
| 阻塞点 | read()/write()/accept() 均阻塞 |
仅 selector.select() 可能阻塞 |
| 系统调用 | 频繁的 read()/write() 系统调用 |
少量 epoll_ctl/epoll_wait 调用 |
| 内核交互 | 每次 I/O 都需用户态 - 内核态切换 | 通过 Buffer 批量处理,减少切换次数 |
| 适用场景 | 连接数少、数据处理快的场景 | 高并发、连接数多的场景(如服务器) |
四、关键底层组件解析
-
Channel(通道):
- 对应操作系统的文件描述符(File Descriptor),是 I/O 操作的抽象(如
SocketChannel对应网络套接字)。 - 支持双向操作(读写),而 BIO 的 Stream 是单向的。
- 对应操作系统的文件描述符(File Descriptor),是 I/O 操作的抽象(如
-
Buffer(缓冲区):
- 本质是一块连续的内存块,封装了数据读写的位置(
position、limit、capacity)。 - 减少用户态与内核态的数据拷贝次数(通过
flip()、clear()等方法高效复用缓冲区)。
- 本质是一块连续的内存块,封装了数据读写的位置(
-
Selector(选择器):
- 底层映射到操作系统的多路复用器(如
epoll),负责监听 Channel 上的事件(连接、读、写)。 - 通过
SelectionKey关联 Channel 与事件,实现事件的分发与处理。
- 底层映射到操作系统的多路复用器(如
总结
- BIO 是简单直观的阻塞模型,依赖线程阻塞等待 I/O,适合低并发场景。
- NIO 基于非阻塞和多路复用,通过 Selector 实现单线程管理多连接,大幅降低资源消耗,适合高并发场景。
- 两者的本质区别在于对操作系统 I/O 模型的封装方式:BIO 直接映射阻塞式 I/O,而 NIO 则利用了内核的多路复用机制,是更高效的 I/O 模型。
更多推荐


所有评论(0)