bigywater.tech
Java · 401

AsynchronousSocketChannel:异步网络 I/O 的完成回调

异步 NIO 的“回调”不是随便在哪里运行的函数。这篇用真实的环回连接量出它在哪个线程上运行、栈有多深、同一个通道上有哪些并发限制、write 与超时的完成语义,以及线程组为什么不终止。

11–139 → 11
默认下回调里的栈深度范围(直接调用嵌套 16 层);sun.nio.ch.maxCompletionHandlersOnStack=0 后恒为 11
319 KiB / 16 MiB
对端不读时,write 一个 16 MiB 的 ByteBuffer 第一次完成写出的字节数
2 个
accept 得到的服务端通道:两个都关闭后线程组才终止(只关闭其中一个时 awaitTermination 仍是 false)

回调线程

实测 · 本机执行输出
  accept 回调运行在线程 [aio-21];read 回调运行在线程 [aio-20](主线程是 main,固定线程池 2 个线程)

直接调用与栈深度

实测 · 本机执行输出
  [默认]
      sun.nio.ch.maxCompletionHandlersOnStack = 默认(未设置):每次读 1 字节、共 5000 次回调,运行在线程 [aio];回调里栈深度最小 11、最大 139,共 17 种深度
        栈深度分布(前几种):[11×295, 19×295, 27×294, 35×294, 43×294, 51×294, 59×294, 67×294]
  [-Dsun.nio.ch.maxCompletionHandlersOnStack=0]
      sun.nio.ch.maxCompletionHandlersOnStack = 0:每次读 1 字节、共 5000 次回调,运行在线程 [aio];回调里栈深度最小 11、最大 11,共 1 种深度
        栈深度分布(前几种):[11×5000]
  [-Dsun.nio.ch.maxCompletionHandlersOnStack=64]
      sun.nio.ch.maxCompletionHandlersOnStack = 64:每次读 1 字节、共 5000 次回调,运行在线程 [aio];回调里栈深度最小 11、最大 523,共 65 种深度
        栈深度分布(前几种):[11×77, 19×77, 27×77, 35×77, 43×77, 51×77, 59×77, 67×77]

并发限制

实测 · 本机执行输出
  已有一个 read 未完成时再调 read:ReadPendingException
  第一个 read 之后完成:3 字节
  对端不读,连续发起 1 MiB 的 write:第 7 个 write 在 50 ms 后仍未完成(此前每个都立即完成了)
  已有一个 write 未完成时再调 write:WritePendingException
  已连接的通道再 connect:AlreadyConnectedException
  未连接就 read:NotYetConnectedException
  用只读 ByteBuffer 做 read 的目标:IllegalArgumentException

部分写

实测 · 本机执行输出
  对端一直不读时,write(16 MiB 的 ByteBuffer) 第一次完成写了 327212 字节(= 319 KiB),缓冲区还剩 16450004 字节,耗时 2 ms
  对端开始读之后,要连续调用 write 10 次才写完 16 MiB(共 16777216 字节)

读超时

实测 · 本机执行输出
  read(300 ms 超时) 的 failed 回调:InterruptedByTimeoutException,用时 313 ms,回调线程 Thread-12
  超时之后再 read:IllegalStateException
  超时之后通道 isOpen = true

回调异常与线程组

实测 · 本机执行输出
  accept 回调在线程 aio-1 里抛出 RuntimeException
  默认 UncaughtExceptionHandler 收到(线程 aio-1):java.lang.RuntimeException: 回调里的异常
  回调抛异常之后:ThreadFactory 一共创建了 2 个线程;下一个 accept 的 Future 在 2 秒内完成了
  group.shutdown() 之后 isShutdown = true,isTerminated = false(还有打开的通道)
  awaitTermination(500 ms) = false
  关闭了客户端通道 cl、cl2 和监听通道 srv 之后 awaitTermination(1 s) = false(accept 得到的两个服务端通道还开着)
  再关闭第二个 accept 得到的通道之后 awaitTermination(1 s) = false
  最后关闭第一个 accept 得到的通道(就是抛异常的回调里拿到的那个)之后 awaitTermination(2 s) = true,isTerminated = true,耗时 2537 ms

分步演示

AsynchronousSocketChannel
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,环回 127.0.0.1:9741;每个场景一个新 JVM
    • 没有验证:AsynchronousFileChannel、多个客户端的并发负载与吞吐(与 NIO Selector 的对比)、Linux(epoll)与 Windows(IOCP)上的差异(这里是 macOS 的 kqueue 实现)、TLS(AsynchronousSocketChannel 本身不支持)、背压与缓冲区管理策略
    京ICP备2026067502号-1京公网安备11010502063487号