IO多路复用
IO 多路复用(I/O Multiplexing) 是面试中的高频问题,也是 Redis、Nginx、Netty、Tomcat 等高性能服务器的核心技术。
一句话理解:
一个线程同时监听多个 Socket,当某个 Socket 有数据可读/可写时,再去处理它,而不是一个连接对应一个线程。
一、为什么需要 IO 多路复用?
假设你要开发一个聊天服务器。
方案一:一个连接一个线程(BIO)
客户端A ───── Thread1
客户端B ───── Thread2
客户端C ───── Thread3
...
客户端10000 ─ Thread10000
Java 代码:
while (true) {
Socket socket = serverSocket.accept();
new Thread(() -> {
while (true) {
socket.getInputStream().read();
}
}).start();
}
存在的问题
假设:
10000 个连接
真正发消息的只有 100 个
9900 个线程都在等待
这些线程:
read()
一直阻塞。
那么:
- 每个线程都需要栈空间(默认约 1MB)
- 线程切换(Context Switch)开销大
- CPU 大量时间浪费在线程调度上
所以:
连接越来越多
↓
线程越来越多
↓
CPU 被线程切换拖死
二、IO 多路复用是如何解决的?
它的思想非常简单:
不要一个线程等一个 Socket,而是一个线程管理很多 Socket。
例如:
线程
│
├── socket1
├── socket2
├── socket3
├── socket4
├── socket5
...
├── socket10000
线程不再:
socket.read()
而是:
select()
或者
epoll_wait()
内核会告诉你:
哪些 socket 有数据
例如:
10000 个连接
只有:
socket3
socket88
socket999
有数据
线程只处理:
socket3.read()
socket88.read()
socket999.read()
其他连接不用管。
三、工作流程
例如:
客户端A
客户端B
客户端C
服务器:
Thread
│
select()/epoll_wait()
│
───────────────────────────────────
SocketA SocketB SocketC
假设:
A没有数据
B有数据
C没有数据
内核返回:
SocketB 可读
线程:
read(SocketB)
处理完成:
继续 select()
四、Linux 的三种 IO 多路复用
1. select
最早出现。
流程:
用户
↓
select()
↓
内核检查所有socket
↓
返回可读socket
问题:
(1) 每次都遍历所有 Socket
例如:
10000 个 socket
每次:
for(socket){
检查有没有数据
}
即使只有一个 Socket 有数据。
时间复杂度:
O(n)
(2) 有最大连接数限制
Linux:
1024
Windows:
64
2. poll
和 select 基本一样。
区别:
没有:
1024
限制。
但是:
仍然遍历全部 Socket
时间复杂度:
O(n)
3. epoll(Linux)
现在互联网几乎都用它。
Redis
Nginx
Netty
Tomcat
RocketMQ
Kafka
基本都基于 epoll。
工作方式
第一次:
epoll_create()
创建:
红黑树
保存所有 Socket。
然后:
epoll_ctl()
注册:
socket
以后:
epoll_wait()
线程睡眠。
如果:
socket123
收到数据
内核主动放到:
Ready List
线程醒来:
直接处理
socket123
不用遍历。
时间复杂度:
O(1)
(更准确地说,返回就绪事件的成本与就绪事件数量相关,而不是与监听的 Socket 总数线性相关。)
五、epoll 为什么快?
例如:
100000 个连接
只有:
20 个连接
有数据
select
100000
↓
全部遍历
↓
找到20个
epoll
内核已经知道:
20个有数据
↓
直接返回20个
所以:
select
100000 次检查
↓
epoll
20 次处理
六、Java 中如何使用?
Java NIO:
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
}
if (key.isReadable()) {
}
}
}
这里:
Selector
就是 Java 对 IO 多路复用的封装。
Linux 下:
Selector
↓
epoll
Windows:
↓
select
七、Redis 为什么是单线程还能支持十几万连接?
Redis 不是一个线程处理一个连接。
它是:
一个线程
↓
epoll
↓
监听几万个 socket
↓
哪个socket有数据
↓
处理哪个socket
例如:
100000 个连接
↓
99950 个空闲
↓
50 个请求
↓
Redis 只处理50个
所以:
CPU 利用率很高
八、Netty 为什么性能高?
Netty 的 EventLoop 本质上就是:
while(true){
selector.select();
收到事件
处理事件
}
一个 EventLoop 可以管理:
几千
甚至
几万个连接
这就是 Netty 能支撑高并发网络通信的关键原因之一。
九、BIO、NIO、AIO 的区别
| 模型 | 是否阻塞 | 一个线程处理 | 并发能力 | 典型应用 |
|---|---|---|---|---|
| BIO | 是 | 一个连接 | 低 | 传统 Java Socket |
| NIO(IO 多路复用) | 非阻塞 | 多个连接 | 高 | Redis、Netty、Nginx |
| AIO | 异步非阻塞 | 多个连接 | 理论更高 | Java 中实际使用较少 |
十、面试中的标准回答(2 分钟版)
IO 多路复用是一种让一个线程同时监听多个 I/O 连接的技术。它通过
select、poll或epoll等机制,将多个 Socket 注册到内核,由内核负责监听这些 Socket 的事件。当某个 Socket 可读、可写或发生其他事件时,内核通知应用程序,应用程序只处理这些就绪的连接,而不是轮询所有连接。在 Linux 中,生产环境几乎都使用 epoll。相比
select和poll,epoll不需要每次遍历所有 Socket,而是维护就绪事件列表,只返回发生事件的连接,因此在大量连接、少量活跃连接的场景下性能非常高。Java 的 NIO Selector 底层在 Linux 上就是基于
epoll实现的,因此像 Redis、Netty、Nginx、Kafka、RocketMQ 等高性能网络框架,都大量使用了 IO 多路复用技术来支持海量并发连接。
浙公网安备 33010602011771号