必须显式调用 wakeup(),因为 select() 阻塞时无法感知用户态的注册变更;注册新 Channel 后需立即 wakeup() 以中断阻塞并触发重新轮询,且须在遍历 selectedKeys 后调用 it.remove() 避免重复处理。

为什么在多线程 Reactor 中必须显式调用 wakeup()
因为 select() 阻塞时,其他线程对 Selector 的注册/取消操作不会自动触发唤醒——它只监听内核事件队列,不感知用户态的注册变更。如果你在工作线程里刚注册了一个新 SocketChannel,而主线程正卡在 selector.select() 上,这个新通道的读事件永远不会被处理,直到下一次超时或别的事件到达。
这不是 bug,是设计使然:select() 的阻塞依赖操作系统底层的事件通知机制(比如 Linux 的 epoll_wait),而 register 是纯 JVM 内存操作,不触发内核事件。所以必须靠 wakeup() 主动“捅”一下,让它退出阻塞、重新轮询注册表。
wakeup() 调用时机:注册后、select() 前还是 select 中?
调用位置其实不关键,只要发生在「注册完成」之后、「下一次 select() 返回就绪」之前即可。但实践中最安全的位置是注册完立刻调用:
- 在工作线程中完成
channel.register(selector, SelectionKey.OP_READ)后,紧跟着写selector.wakeup() - 不要等主线程自己醒来再注册,那会丢失事件窗口
- 也不必判断 selector 是否正在阻塞——
wakeup()是幂等的,未阻塞时调用无副作用 - 避免在
select()循环内部、还没处理完selectedKeys()时调用,否则可能干扰当前轮次的 key 遍历逻辑
常见误用:wakeup 后没清空 selectedKeys 导致重复处理
wakeup() 不会清空 selectedKeys() 集合,它只中断阻塞。如果主线程刚被唤醒,又立即进入下一轮 select(),而上一轮的 keys 还没移除,就会重复遍历同一个 SelectionKey,造成逻辑错乱(比如重复 accept、重复 read)。
必须坚持这个顺序:
- 调用
selector.select()(或被wakeup()中断后返回) - 拿到
selector.selectedKeys() - 用
Iterator遍历,每处理一个 key 就调用it.remove() - 不能用 for-each 循环直接遍历
selectedKeys(),否则抛ConcurrentModificationException
性能与线程安全注意事项
wakeup() 是线程安全的,可被任意线程并发调用,多次调用等价于一次——底层通过 interruptTriggered 标志位控制,不会堆积唤醒信号。
但它有轻微开销:每次调用都会向内部 wakeup socket 写入一个字节,触发一次内核事件。所以别滥用,比如在每条消息发送后都 wakeup();只在真正需要让 selector 立即响应注册变更时才用。
另外注意:selectNow() 可作为替代方案用于低延迟场景,但它不阻塞,意味着你要自己控制 CPU 占用(比如加 Thread.yield() 或短休眠),而 wakeup() + select() 组合更符合标准 Reactor 模式语义。
最易被忽略的一点:wakeup 之后,select() 返回值可能是 0——这不代表没事件,只是说明「唤醒是人为触发的,不是内核通知的」,你仍要检查 selectedKeys() 是否非空,不能只靠返回值做分支判断。


















