从线程模型到 Selector 多路复用,彻底理解 I/O 模型的演进
accept()/ read()后,线程原地卡死,数据不来永远等着,什么都干不了。accept()/ read()后,线程立刻返回,有就处理,没有就接着干别的。| 阻塞(默认) | 非阻塞 | |
|---|---|---|
accept() |
没人连 → 卡死不动 | 没人连 → 立刻返回 null |
有人连 → 返回 SocketChannel |
有人连 → 返回 SocketChannel |
|
read() |
没数据 → 卡死不动 | 没数据 → 立刻返回 0 |
| 有数据 → 返回读到的字节数 | 有数据 → 返回读到的字节数 |
线程一旦进入 read(connA),就卡死在里面,根本没机会回去调下一次 accept()。后面的客户端 B 连上了也无人理睬。
要解决只能:每 accept 一个连接,立刻丢给一个新线程去慢慢 read,主线程赶紧回去继续 accept。
1000 个连接 → 1000 个线程 → 每个线程栈默认 1MB → 光栈内存就 1GB,线程切换也超级费。
这就是阻塞模型的致命缺陷:连接数 = 线程数,根本无法支撑高并发场景。
所有调用都不卡,线程可以在 N 个连接之间飞速跳来跳去:
← 一轮循环,全照顾到了,谁都不耽误 →
非阻塞轮询就像一根飞速转的棍子,只要转得够快,每个连接有数据的瞬间都能被"拦截"到。
但棍子转得再快,99% 的时间你问每个连接「有数据吗?」,回答都是「没」,CPU 在空转,大部分转圈是白费的。
所以有了第三层 — Selector:
| 棍子怎么转 | CPU 消耗 | 延迟 | |
|---|---|---|---|
| 阻塞 | 不转,蹲一个人面前不走 | 低(线程挂起) | 其他连接延迟无限大 |
| 非阻塞轮询 | 自己疯狂转,挨个问 | 高(大部分白问) | 取决于棍子转多快 |
| Selector(多路复用) | OS 帮你转,有事才叫你 | 低(没事件就睡) | 低(事件到达立刻通知) |
第一步解决"能管多个" → 第二步解决"不白费 CPU"
| 阻塞模式 | 非阻塞轮询 | Selector |
|---|---|---|
| 你站门口死等一个人 | 你来回跑挨个房间问 | 所有人给你发微信 |
| 其他人完全不管 | CPU 累死,大部分白问 | 谁有动静你才去看谁,平时躺着 |
操作系统当你的「秘书」,在内核层面帮你同时盯着所有连接,有事件才通知你。你不用自己转棍子了。
select()— 没事件就睡,不烧 CPU
select()返回,拿到就绪的 Key 集合
// ========== 阻塞模式 ==========
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
while (true) {
SocketChannel conn = ssc.accept(); // ← 卡死
new Thread(() -> {
conn.read(buf); // ← 卡死
}).start();
}
// ========== 非阻塞模式 ==========
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
while (true) {
SocketChannel conn = ssc.accept(); // ← 立刻返回(null 或连接)
if (conn != null) {
conn.configureBlocking(false);
int n = conn.read(buf); // ← 立刻返回(0 或有数据)
}
}
// ========== Selector 模式 ==========
Selector sel = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.register(sel, SelectionKey.OP_ACCEPT); // 让 OS 帮你盯着
while (true) {
sel.select(); // ← 没事件就睡,不烧 CPU
for (SelectionKey key : sel.selectedKeys()) {
if (key.isAcceptable()) { /* 新连接 */ }
if (key.isReadable()) { /* 有数据 */ }
}
}
| 特性 | 阻塞模式 | 非阻塞轮询 | Selector |
|---|---|---|---|
| 配置方式 | 默认 | configureBlocking(false) |
configureBlocking(false)+ register(sel, ops) |
| accept 行为 | 卡死等连接 | 立刻返回 null 或连接 | Selector 通知有新连接 |
| read 行为 | 卡死等数据 | 立刻返回 0 或字节数 | Selector 通知可读 |
| 线程模型 | 1 连接 : 1 线程 | 1 线程 : N 连接 | 1 线程 : N 连接 |
| CPU 消耗 | 低(挂起) | 高(空转轮询) | 低(事件驱动) |
| 适用场景 | 连接少、逻辑简单 | 不推荐(过渡方案) | 高并发、生产环境 |
read()绑架了,不拿到数据不放人。一个连接必须独占一个线程。阻塞是线程被 read() 绑架了,不拿到数据不放人;非阻塞是线程随时脱身,一根棍子巡视全场;Selector 是 OS 替你转棍子,省下 CPU 干正事。
Netty、Redis、Nginx、Tomcat NIO 能支撑几万并发连接,底层全是这套 Selector 机制。