BIO、NIO 与多路复用
一个服务要扛一万个连接。BIO 的写法是一个连接一个线程,一万个线程,每个都在干等数据来。光是线程栈就吃掉几个 GB,CPU 忙着在它们之间倒手,业务逻辑没跑几行。NIO 和多路复用解决的就是这笔账,演进的主线只有一个问题:等待这件事,谁来干。
BIO:线程干等
BIO 的 accept、read、write 全是阻塞的。线程调 read 就挂在那,数据不来不动窝,来了才醒。模型简单直观,代价是「一个连接一个线程」,线程数跟着连接数走。连接大部分是闲的,线程大部分在睡,睡也是要占内存、要调度的。
NIO:自己轮询
Java NIO 给了三件套,Channel(通道)、Buffer(缓冲区)、Selector(选择器)。连接设成非阻塞之后,read 没数据立刻返回,线程可以去问下一个连接。Selector 就是「一次问一圈」的工具,一个线程管理成千上万个 Channel,谁就绪了处理谁。
账变了,线程数不再跟连接数走。但 Selector 底下的实现在操作系统层面有个演进过程,这决定了「问一圈」的效率。
多路复用:让内核代看
select 和 poll 是第一代。每次调用都把整个连接列表从用户态拷给内核,内核挨个看一遍谁就绪,返回后程序再挨个找。一万连接看一圈是 O(n),大部分连接还是没数据,白看。
epoll 是第二代。连接注册一次,之后内核只把「就绪的」推给你,看几个拿几个,O(活跃数)。连接再多,活跃的就那么些,账就平了。
#mermaid-svg-OzNfCYJ6I2R2SaKC{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OzNfCYJ6I2R2SaKC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OzNfCYJ6I2R2SaKC .error-icon{fill:#552222;}#mermaid-svg-OzNfCYJ6I2R2SaKC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OzNfCYJ6I2R2SaKC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OzNfCYJ6I2R2SaKC .marker.cross{stroke:#333333;}#mermaid-svg-OzNfCYJ6I2R2SaKC svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OzNfCYJ6I2R2SaKC p{margin:0;}#mermaid-svg-OzNfCYJ6I2R2SaKC .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-OzNfCYJ6I2R2SaKC .cluster-label text{fill:#333;}#mermaid-svg-OzNfCYJ6I2R2SaKC .cluster-label span{color:#333;}#mermaid-svg-OzNfCYJ6I2R2SaKC .cluster-label span p{background-color:transparent;}#mermaid-svg-OzNfCYJ6I2R2SaKC .label text,#mermaid-svg-OzNfCYJ6I2R2SaKC span{fill:#333;color:#333;}#mermaid-svg-OzNfCYJ6I2R2SaKC .node rect,#mermaid-svg-OzNfCYJ6I2R2SaKC .node circle,#mermaid-svg-OzNfCYJ6I2R2SaKC .node ellipse,#mermaid-svg-OzNfCYJ6I2R2SaKC .node polygon,#mermaid-svg-OzNfCYJ6I2R2SaKC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OzNfCYJ6I2R2SaKC .rough-node .label text,#mermaid-svg-OzNfCYJ6I2R2SaKC .node .label text,#mermaid-svg-OzNfCYJ6I2R2SaKC .image-shape .label,#mermaid-svg-OzNfCYJ6I2R2SaKC .icon-shape .label{text-anchor:middle;}#mermaid-svg-OzNfCYJ6I2R2SaKC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OzNfCYJ6I2R2SaKC .rough-node .label,#mermaid-svg-OzNfCYJ6I2R2SaKC .node .label,#mermaid-svg-OzNfCYJ6I2R2SaKC .image-shape .label,#mermaid-svg-OzNfCYJ6I2R2SaKC .icon-shape .label{text-align:center;}#mermaid-svg-OzNfCYJ6I2R2SaKC .node.clickable{cursor:pointer;}#mermaid-svg-OzNfCYJ6I2R2SaKC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OzNfCYJ6I2R2SaKC .arrowheadPath{fill:#333333;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OzNfCYJ6I2R2SaKC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OzNfCYJ6I2R2SaKC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OzNfCYJ6I2R2SaKC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OzNfCYJ6I2R2SaKC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OzNfCYJ6I2R2SaKC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OzNfCYJ6I2R2SaKC .cluster text{fill:#333;}#mermaid-svg-OzNfCYJ6I2R2SaKC .cluster span{color:#333;}#mermaid-svg-OzNfCYJ6I2R2SaKC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OzNfCYJ6I2R2SaKC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OzNfCYJ6I2R2SaKC rect.text{fill:none;stroke-width:0;}#mermaid-svg-OzNfCYJ6I2R2SaKC .icon-shape,#mermaid-svg-OzNfCYJ6I2R2SaKC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OzNfCYJ6I2R2SaKC .icon-shape p,#mermaid-svg-OzNfCYJ6I2R2SaKC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OzNfCYJ6I2R2SaKC .icon-shape .label rect,#mermaid-svg-OzNfCYJ6I2R2SaKC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OzNfCYJ6I2R2SaKC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OzNfCYJ6I2R2SaKC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OzNfCYJ6I2R2SaKC :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
epoll
连接注册一次
内核只推就绪的
拿几个处理几个
select / poll 每轮
拷全量连接表进内核
内核挨个问:你有数据吗
返回后程序再挨个筛
Netty 站在这层肩膀上。它把 Reactor 模式(事件来了分发给处理者)封装好,加上线程模型、缓冲区管理和协议编解码,让你写业务而不是写 Selector 循环。你在用的中间件,消息队列客户端、网关、SSE 流式输出,底下大都是这条路。
亲手看账单
下面用一段 Java 起一个单线程的 Selector 服务,一百个客户端连接各发一条消息,看一个线程怎么扛下全部读写。直接运行 java IoBill.java(JDK 11+)。
// 直接运行:java IoBill.java
// 一个 Selector 线程扛 100 个连接,对照 BIO 的一线程一连接。
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.concurrent.atomic.AtomicInteger;
public class IoBill {
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress("127.0.0.1", 0));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
int port = ((InetSocketAddress) ssc.getLocalAddress()).getPort();
AtomicInteger handled = new AtomicInteger();
// 唯一的 reactor 线程:accept + read + echo
Thread reactor = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
selector.select(100);
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
SocketChannel ch = ((ServerSocketChannel) key.channel()).accept();
ch.configureBlocking(false);
ch.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel ch = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(64);
if (ch.read(buf) > 0) {
buf.flip();
ch.write(buf);
handled.incrementAndGet();
} else {
ch.close();
}
}
}
}
} catch (IOException e) {
// 关闭时退出
}
}, "single-reactor");
reactor.start();
// 100 个客户端,各发一条消息并等 echo
Thread[] clients = new Thread[100];
for (int i = 0; i < clients.length; i++) {
clients[i] = new Thread(() -> {
try (SocketChannel ch = SocketChannel.open(new InetSocketAddress("127.0.0.1", port))) {
ch.write(ByteBuffer.wrap("ping".getBytes()));
ByteBuffer buf = ByteBuffer.allocate(8);
ch.read(buf);
} catch (IOException e) {
// 演示不处理
}
});
clients[i].start();
}
for (Thread t : clients) {
t.join();
}
System.out.println("单一 reactor 线程处理了 " + handled.get() + " 次读写");
System.out.println("BIO 对照:同样 100 个连接要 100 个阻塞线程,栈内存与切换开销乘 100。");
reactor.interrupt();
selector.wakeup();
}
}
记住一句话:IO 模型的演进,是「谁在等」的演进,从线程干等,到程序自己看,再到内核代看。
适用边界
这一篇管 IO 模型的账。epoll 的惊群和 LT/ET 触发模式、Netty 的线程模型细节,以及虚拟线程对 BIO 的复兴(JDK 21 之后写阻塞代码也能高并发),是另外的题。演示用的是单线程 Selector,真实服务是 Reactor 主从多线程,账的量级不变。跑完演示再想那句「一万个连接」,选型时就知道自己的服务站在哪一层。
网硕互联帮助中心



评论前必须登录!
注册