关键在于非阻塞是注册到selector的硬性前提,因epoll/kqueue仅支持非阻塞文件描述符;selectablechannel默认阻塞,须configureblocking(false)后才能register,否则抛illegalblockingmodeexception。

关键不在“用了 Selector”,而在于 SelectableChannel 必须是非阻塞的,否则整个事件驱动模型就塌了——它不是优化技巧,而是操作系统多路复用机制(epoll/kqueue)的硬性要求。
非阻塞是注册到 Selector 的前提条件
ServerSocketChannel、SocketChannel、DatagramChannel 都继承自 SelectableChannel,但默认全是阻塞模式。open() 后不调 configureBlocking(false),直接 register() 就会抛 IllegalBlockingModeException。这不是 Java 层面的限制,而是因为:
- Linux 的 epoll、macOS 的 kqueue 只能监听非阻塞文件描述符
- 阻塞通道注册后,select() 虽不报错,但后续 read()/write() 可能卡住整个事件循环线程
- FileChannel 不可注册,正因为它不支持非阻塞 I/O,也不继承 SelectableChannel
事件驱动靠的是“状态变化通知”,不是轮询忙等
Selector 本身不干活,它只是把多个通道的就绪状态聚合起来。真正驱动逻辑的是通道自身状态的切换:
- ServerSocketChannel 接收到 SYN 包 → 内核标记“可 accept” → Selector 返回 OP_ACCEPT
- SocketChannel 收到 TCP 数据包 → 内核缓冲区有数据 → 标记“可 read” → Selector 返回 OP_READ
- SocketChannel 发送缓冲区腾出空间 → 标记“可 write” → Selector 返回 OP_WRITE(但这个要慎用)
整个过程无需线程等待 I/O 完成,线程只在状态真正就绪时才介入处理。
OP_READ 和 OP_WRITE 的注册必须动态调整
静态注册容易引发性能陷阱。比如一上来就同时注册 OP_READ | OP_WRITE,会导致 select() 频繁返回,但实际既没数据可读、也没空间可写——这就是“虚假就绪”。正确做法是:
- 新 accept 进来的 SocketChannel,只注册 OP_READ(客户端大概率先发请求)
- 当 write() 返回 0(内核发送缓冲区满),再临时追加 OP_WRITE
- 一旦写出部分数据,立刻取消 OP_WRITE,避免持续唤醒
- OP_WRITE 永远不该长期保留在 interestOps 中
selectedKeys() 遍历后必须手动 remove()
Selector 返回的 selectedKeys() 是一个由它内部管理的 Set,每次 select() 返回的是“新增就绪”的 key 列表,但不会自动清理。如果漏掉 keyIterator.remove():
- 同一个 key 会在下一轮 select() 后重复出现,导致重复 accept、重复 read
- 若多线程误操作 selector,可能触发 ConcurrentModificationException
- 不能用 for-each 遍历 selectedKeys(),因为它隐式创建 iterator 却无法调用 remove()
必须用显式 Iterator,并在处理完每个 key 后立即调用 remove()。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











