Selector 通过操作系统内核的 I/O 多路复用(如 Linux epoll)实现高效轮询,select() 阻塞等待内核通知就绪 Channel,仅处理 selectedKeys() 中的活跃事件,避免遍历全部 Channel;注册前须设为非阻塞并指定 interestOps;迭代 selectedKeys() 后必须手动 remove();单 Selector 瓶颈在 JVM 内存、GC 和业务逻辑耗时,应分离 IO 与业务处理。

Selector 是怎么轮询成千上万个 Channel 的
它不靠线程遍历,而是依赖操作系统内核的 I/O 多路复用机制(Linux 上是 epoll,Windows 上是 IOCP 封装)。Selector.select() 调用会阻塞,直到至少一个注册的 Channel 就绪(比如有数据可读、可写、连接完成),这时内核把就绪列表返回给 JVM,Selector.selectedKeys() 返回的就是这批活跃事件的集合——你只处理真正需要响应的连接,而不是每毫秒扫一遍十万条连接。
关键点在于:就绪通知由内核触发,不是 Java 主动轮询;一次 select() 可能唤醒多个 Channel,但你只迭代 selectedKeys() 里那几十或几百个,复杂度从 O(N) 降到 O(1)~O(K),K 是就绪数。
注册 Channel 时必须禁用阻塞模式并显式关注事件
SocketChannel 默认是阻塞的,直接注册到 Selector 会抛 IllegalBlockingModeException。而且,不指定 interestSet 就等于没注册任何事件,这个 Channel 永远不会出现在 selectedKeys() 中。
- 必须先调用
channel.configureBlocking(false) - 再用
channel.register(selector, SelectionKey.OP_READ)注册,不能漏掉第二个参数 - 如果后续要写数据,别在
OP_READ事件里直接channel.write()——可能写不完,得改用key.interestOps(SelectionKey.OP_WRITE)并确保OP_WRITE可用(注意:epoll 中OP_WRITE就绪条件很宽松,几乎总是就绪,慎用)
selectedKeys() 迭代后必须手动 remove,否则下次 select 会重复触发
Selector 内部用一个 SelectedSelectionKeySet 缓存就绪 key,每次 select() 返回前会把新就绪的 key 加进去,但不会自动清理已处理的 key。如果你只遍历不 remove(),同一个 key 会在下一轮 select() 后再次出现,导致重复处理甚至死循环。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
立即学习“Java免费学习笔记(深入)”;
正确做法是用迭代器边遍历边删:
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须这一步
if (key.isReadable()) handleRead(key);
if (key.isWritable()) handleWrite(key);
}
十万连接下 Selector 自身的性能瓶颈和规避点
单个 Selector 在 Linux 上用 epoll 实现,理论支持百万级连接,但实际卡在三个地方:JVM 堆内存(每个 SelectionKey 约 100+ 字节)、GC 压力(频繁创建/销毁 key)、以及单线程处理逻辑的 CPU 占用。当业务逻辑稍重(比如 JSON 解析、DB 查询),单线程立刻成为瓶颈,连接数上不去。
- 避免在
select()循环里做耗时操作;IO 事件只做 read/write,业务逻辑扔给线程池 - 用
ByteBuffer池(如 Netty 的PooledByteBufAllocator)减少 GC - 不要为每个连接 new 一个
Thread,但可以按需分发到固定线程池——这不是违背单线程 IO 模型,而是 IO 线程只管调度,不干重活 - 注意
Selector.wakeup()频繁调用会导致select()提前返回,增加空转,仅在外部线程需中断阻塞时谨慎使用
真正难的不是撑住十万连接,而是让这十万连接持续低延迟收发——大部分坑藏在业务逻辑的同步阻塞和堆外内存管理里,而不是 Selector 本身。

















