bigywater.tech
Java · 148

Java NIO:Selector 如何同时管理成千上万个连接

Selector 为什么能用一个线程管成千上万个连接?这篇用同样的 3000 个连接对比 Selector 与每连接一线程的资源占用,再把水平触发和 OP_WRITE 两个常见的空转陷阱量出来(Netty 的粘包拆包、Selector.wakeup 在后面的篇目)。

7 vs 3007
Selector 版与每连接一线程版,3000 个连接时的 Java 线程数
53 → 287 MB
同样 3000 个连接下的进程 RSS
640163
有数据不读时 200 ms 内 selectNow 空转的次数

一个线程管 3000 个连接

实测 · 本机执行输出
  3000 个连接,每个发 64 字节并收到回显:(每批最多 100 个在途,连接全部保持打开)总耗时 134 ms;服务端只用了 1 个线程(selector-thread);accept 了 3000 个
  服务端 select() 返回了 4511 次,每次平均处理 1.1 个就绪的通道,最多一次 45
  此时 JVM 里的 Java 线程数 7(含 main 与 JVM 自己的几个系统线程),进程 RSS 53 MB(3000 个连接都还开着;客户端也在这个进程里)

每连接一个线程

实测 · 本机执行输出
  3000 个连接(同样每批最多 100 个在途,全部保持打开):总耗时 275 ms;服务端创建了 3000 个处理线程(accept 了 3000 个);创建线程失败:没有
  此时 JVM 里的 Java 线程数 3007,进程 RSS 287 MB(3000 个连接都还开着;客户端也在这个进程里)

水平触发与 OP_WRITE

实测 · 本机执行输出
  不读数据:200 ms 内 select 返回了「有就绪通道」的次数 = 640163
  读掉数据:200 ms 内 select 返回了「有就绪通道」的次数 = 1
  对已读完的连接再 select:200 ms 内 select 返回了「有就绪通道」的次数 = 0
  注册了 OP_WRITE 却没有东西要写:200 ms 内 select 返回「就绪」的次数 = 636177(socket 发送缓冲一直有空位,所以一直就绪)

分步演示

Selector 多路复用
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,客户端与服务端在同一个 JVM 里(回环地址),连接分批建立(每批最多 100 个在途)
    • 没有验证:epoll 与 kqueue 实现的差别(本机是 macOS 的 kqueue)、连接数上万或几十万时的表现、线程数撞系统上限时的报错、真实网络下的延迟;RSS 来自 ps,包含客户端那一侧
    京ICP备2026067502号-1京公网安备11010502063487号