selector 不是 epoll 的简单封装,而是 jvm 重定义的抽象层,select() 默认不阻塞,因设计为可中断+可超时的协作式等待;interestops 与 readyops 语义分离,需显式更新;jvm 固定使用水平触发且隐藏底层控制权,易引发 fd 泄漏与就绪误判。

Selector 不是 epoll 的简单封装,它是 Java 抽象层,底层在 Linux 上确实用 epoll,但行为、生命周期和错误语义都经过 JVM 重定义——直接套用 epoll 手册理解 Selector 会踩坑。
为什么 Selector.select() 有时不阻塞?
常见现象:线程没等事件就立刻返回,或只等几毫秒就醒;你以为它该“一直等”,其实它默认不阻塞。根本原因是 select() 方法本身设计为“可中断 + 可超时”的协作式等待,不是系统调用级的死等。
-
select()等价于select(0):无超时,但会响应Selector.wakeup()、中断、新注册通道、甚至 JVM 内部信号 - 真正阻塞需显式调用
select(long timeout),且 timeout > 0;注意单位是毫秒,不是纳秒 - 如果之前调用了
selector.wakeup()但没被消费,下一次select()会立即返回(这是常见漏处理点) - JVM 在某些 GC 阶段(如 ZGC 的并发标记)可能插入唤醒,导致非预期返回
SelectionKey 的 interestOps 和 readyOps 别混用
interestOps 是你“想要监听什么”,readyOps 是内核告诉你“现在能干啥”——它们不是实时镜像,也不自动同步。
- 修改 interest 需调用
key.interestOps(int ops)或key.interestOps(int ops).attach(...),不能直接改字段 -
readyOps只在select()返回后有效,且只反映本次就绪状态;下次select()前不会清零,也不会自动更新 - 如果通道就绪后你没调用
key.channel().read(...)消费数据,readyOps可能持续包含OP_READ(比如 socket 缓冲区还有数据) - 别在多线程里并发修改同一个
SelectionKey的 interestOps:不是线程安全的,JVM 不保证原子性
Linux 上 epoll_wait() 被封装后丢失了哪些控制权?
Java 层看不到 epoll_ctl() 的 EPOLLET(边缘触发)标志,也没法控制 epoll_wait() 的 maxevents——这些都被 JVM 固定策略接管了。
立即学习“Java免费学习笔记(深入)”;
- JVM 总是以
EPOLLLT(水平触发)模式使用 epoll,所以你不需要(也不能)手动循环读到EAGAIN;但这也意味着:只要缓冲区有数据,OP_READ就一直就绪 - 每次
select()调用,JVM 内部会分配固定大小的事件数组(通常 256~1024),超出部分被截断——如果你注册了几万通道,得靠多次select()轮询,不是一次全返回 - 没有暴露
EPOLLONESHOT,无法实现“一次性通知”;想模拟就得自己维护 key 状态 + 手动 cancel/re-register - 文件描述符泄漏风险更高:一旦
Channel.close()失败或没调用,对应 fd 不会从 epoll 实例中移除,而 Java 层很难监控 fd 数量
最麻烦的是:Selector 的“就绪”语义和操作系统不完全对齐——比如 TCP 连接关闭时,OP_READ 可能先就绪一次(读到 0 字节),之后才触发 OP_CONNECT 失败或 key.isValid() == false。这个间隙期,业务逻辑容易误判连接状态。










