Selector 用单线程监控海量连接的本质是委托内核(如 Linux 的 epoll_wait)只返回就绪 fd,而非轮询;通道必须 configureBlocking(false) 才能注册,否则破坏事件驱动模型,且需正确使用 Buffer 状态和 OP_WRITE。

Selector 是怎么用一个线程盯住成百上千个连接的?
本质不是“轮询每个连接”,而是把监控权交给操作系统内核——Java 的 Selector 在 Linux 上底层调用的是 epoll_wait(Java 7+),它不遍历所有连接,只返回真正就绪的那几个 fd(文件描述符)。所以哪怕注册了 10 万个 SocketChannel,只要只有 3 个有数据可读,select() 就只返回这 3 个对应的 SelectionKey。
- 必须把通道设为非阻塞:
channel.configureBlocking(false),否则注册会直接抛IllegalBlockingModeException - 注册时只能指定“感兴趣事件”:
OP_ACCEPT、OP_READ、OP_WRITE等,不能混用;比如OP_WRITE多数时候是“永远就绪”的,盲目注册会导致空转 -
select()默认是阻塞的,但你可以传超时参数:selector.select(1000),避免线程被卡死
为什么 Channel 必须先 configureBlocking(false) 才能注册到 Selector?
因为 Selector 的设计前提就是“事件驱动 + 非阻塞”。如果通道还是阻塞模式,当调用 read() 或 write() 时仍可能挂起线程,那就彻底破坏了单线程复用的根基——你没法在同一个线程里既等系统通知、又自己卡在 IO 上。
- 常见错误:忘了这一步,结果
register()抛异常,或者看似注册成功,但后续select()永远不返回就绪 key -
ServerSocketChannel和SocketChannel都要设,且必须在register()前完成 - 一旦设为非阻塞,所有 IO 操作(
read()/write())都立即返回,返回值为 0、-1 或实际字节数,需手动判断是否就绪或结束
Buffer.flip() 和 clear() 不小心用错,数据就丢了
Buffer 不是普通数组,它的 position、limit、capacity 三者状态决定了你能读到哪、写到哪。最典型翻车点:写完没 flip() 就去读,或者读完没 clear() / compact() 就继续写,结果读不到数据,或新数据覆盖旧数据。
- 写流程固定三步:
buffer.put(...)→buffer.flip()→channel.write(buffer) - 读流程也是三步:
channel.read(buffer)→buffer.flip()→buffer.get(...)(注意:read 后 buffer 是“写模式”,position 指向末尾,必须 flip 才能读) - 处理半包时别用
clear()(会丢未读完的数据),改用compact():把未读数据移到开头,position 设为已读长度,方便下次续读
OP_WRITE 为什么总是就绪?该怎么安全使用?
OP_WRITE 表示“通道底层缓冲区有空间可写”,而 TCP 发送缓冲区默认很大(几十 KB),刚建立连接后几乎总是空的,所以注册 OP_WRITE 后 select() 很快就返回——但这不代表你该立刻狂写,更不代表对方一定能收到。
- 滥用
OP_WRITE是高频 CPU 占用元凶:一就绪就 write,写不完又触发下一次就绪,形成空转循环 - 正确做法:只在 write 返回值 OP_WRITE;等就绪后再尝试继续写;一旦写完,立刻取消注册:
key.interestOps(SelectionKey.OP_READ) - 不要在 accept 后自动注册
OP_WRITE,除非你明确要发握手响应(如 HTTP 101)

















