Selector通过I/O多路复用(epoll/kqueue)实现单线程管理上万连接,依赖非阻塞SocketChannel、正确注册事件(OP_ACCEPT仅用于ServerSocketChannel,OP_READ/OP_WRITE用于SocketChannel)、及时清理selectedKeys、对象池化ByteBuffer及wakeup()协调线程安全。

Selector 是怎么“同时”管上万个连接的
它根本不是真并发,而是操作系统级别的 I/O 多路复用(epoll/kqueue/select)在 Java 层的封装。Selector 本身不持有连接,只监听 SelectionKey 上注册的就绪事件(比如 OP_READ、OP_ACCEPT)。真正承载连接的是 SocketChannel,而每个 channel 必须设置为非阻塞模式(configureBlocking(false)),否则注册会直接抛 IllegalBlockingModeException。
常见错误现象:java.nio.channels.IllegalBlockingModeException —— 90% 是忘了调 configureBlocking(false);还有人误以为调一次 select() 就能处理完所有就绪通道,结果漏读、卡住或 CPU 空转。
- 必须在
SocketChannel创建后、注册前调用configureBlocking(false) -
Selector.select()是阻塞的,但可传超时参数(如select(100)),避免无限等待 - 每次
select()返回后,要遍历selectedKeys()集合,处理完记得调iterator.remove(),否则下次还会出现 - 不要在
selectedKeys()迭代过程中直接remove()元素(会抛ConcurrentModificationException),用Iterator.remove()
注册 OP_ACCEPT 和 OP_READ 的时机不能混
服务器启动后,只有 ServerSocketChannel 需注册 OP_ACCEPT;一旦 accept() 成功,得到的 SocketChannel 才注册 OP_READ(或 OP_WRITE,如果需要立即回写)。把 OP_ACCEPT 和 OP_READ 混在同一个 channel 上注册,会导致逻辑错乱甚至 NPE。
典型误用:serverChannel.register(selector, SelectionKey.OP_ACCEPT | SelectionKey.OP_READ) —— ServerSocketChannel 根本不支持 OP_READ,注册后 key 会失效,后续 select() 可能返回空就绪集。
-
ServerSocketChannel只注册OP_ACCEPT -
SocketChannel接收连接后,注册OP_READ(若需写数据,再临时追加OP_WRITE,用完立刻取消,避免持续触发) - 注册
OP_WRITE要谨慎:TCP 缓冲区未满时它几乎总是就绪,容易导致忙等;更适合用于“写半包”后等待缓冲区腾出空间
处理数万连接时,ByteBuffer 分配和回收是性能瓶颈
每条连接都配一个独立 ByteBuffer(尤其堆外 buffer)会吃光 DirectMemory,而反复 new ByteBuffer 又触发 GC 压力。JDK 自带的 ByteBuffer.allocateDirect() 不回收内存,allocate() 又受堆限制。
真实场景中,10k 连接 × 8KB buffer = 80MB 堆外内存,这还只是读缓冲——写缓冲另算。更麻烦的是,不同连接的读写节奏差异大,静态分配易浪费或溢出。
- 用对象池管理
ByteBuffer,例如 Netty 的PooledByteBufAllocator或 Apache Commons Pool 包装allocateDirect() - 避免对每个 channel 都长期持有大 buffer;可按需 resize,或用
compact()+flip()复用同一块空间 - 读取时优先用
read(ByteBuffer),别用read(ByteBuffer[], int, int)(gather 操作开销大,且多数场景用不上) - 注意
ByteBuffer.isDirect()的判断成本低,但频繁调用仍可省则省
Selector.wakeup() 和线程安全边界在哪
wakeup() 是唯一能安全中断阻塞中 select() 的方式,常用于外部线程提交新任务(比如新连接接入、定时任务触发)后唤醒 selector 线程。但它不是“发信号”,而是“让本次 select() 提前返回”,且只会生效一次——连续两次 wakeup() 后只中断一次 select()。
容易被忽略的点:Selector 实例不是线程安全的,所有注册、取消、修改 interestOps 必须在 selector 所在线程执行(或通过 wakeup() + 队列协调)。跨线程直接调 key.interestOps(OP_READ) 会抛 CancelledKeyException 或静默失败。
- 所有对
SelectionKey的操作(包括cancel()、interestOps())必须在 selector 线程内完成 - 外部线程可通过无锁队列(如
ConcurrentLinkedQueue)投递任务,selector 线程在每次select()返回后检查并执行 -
wakeup()后不必担心重复唤醒开销,但也不该高频调用(比如每毫秒一次),它涉及系统调用 - Windows 下
select()实现有已知 bug,高并发时可能漏事件,建议生产环境用 Linux + epoll(JDK 7+ 默认)
真正难的不是写通 Selector,而是让成千上万个 channel 的生命周期、buffer 管理、异常恢复、线程协作全部不互相干扰。很多问题不会立刻报错,而是在连接突增、GC 暂停、网络抖动时才暴露——比如某个 channel 的 close() 没触发 key.cancel(),残留 key 占着资源,慢慢拖垮整个 selector 循环。

















