阻塞 IO vs 非阻塞 IO
read() 方法会「卡住」多久?这是阻塞和非阻塞的核心区别。
阻塞 IO(BIO)
调用 read() 时,如果没有数据,线程就一直等待,直到:
- 数据来了,返回读到的字节数
- 流关闭了,返回 -1
- 出错了,抛出异常
java
// BIO 模式:没数据就一直等
ServerSocket server = new ServerSocket(8080);
Socket socket = server.accept(); // 阻塞:等客户端连接
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf); // 阻塞:等客户端发数据BIO 的问题是:一个线程只能处理一个连接。10000 个连接就需要 10000 个线程,线程切换开销巨大。
非阻塞 IO(NIO)
调用 read() 时,如果没有数据,立刻返回 -1 或 0,不会等待。
java
// NIO 模式:非阻塞
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false); // 设置为非阻塞模式
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer); // 立即返回,没数据返回 0 或 -1但光非阻塞没用——你还是得不停地轮询「有没有数据」,这叫忙等(busy wait),浪费 CPU。
多路复用:非阻塞的真正价值
光非阻塞不够,还需要多路复用器(Selector)来监听多个 Channel,只在有事件时才真正去读写。
java
// 多路复用:用 Selector 在一个线程里监听多个连接
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 监听 accept 事件
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ); // 监听 read 事件
// 阻塞在这里,等任意一个 Channel 有事件就返回
while (selector.select() > 0) {
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
// 有新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// 有数据可读
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer); // 不会卡死,因为已经确认有数据
}
}
selector.selectedKeys().clear();
}四种 IO 模型对比
| 模型 | 阻塞 | 非阻塞 | 同步 | 异步 |
|---|---|---|---|---|
| 同步阻塞(BIO) | ✅ | ❌ | ✅ | ❌ |
| 同步非阻塞(NIO) | ❌ | ✅ | ✅ | ❌ |
| 异步阻塞 | ✅ | ❌ | ❌ | ✅(没人用) |
| 异步非阻塞(AIO) | ❌ | ✅ | ❌ | ✅ |
实际上只有三种有意义的组合:BIO、NIO、AIO。
生活中的类比
- BIO:你去餐厅吃饭,服务员在你桌边等着你点菜,点完才走开
- NIO(无复用):你去餐厅吃饭,服务员每隔 10 秒来问你一次「点菜了吗」
- NIO(多路复用):你去餐厅吃饭,领班用对讲机监听所有桌,「有人举手才过去服务」
记住这个结论:Selector 是 NIO 的精髓——一个线程管 N 个连接。
