BIO/NIO/AIO 与多路复用

共 85 题
📑 题目列表 85 题
#
★★★

1. AIO 与 Netty 的边界

Java AIO(NIO.2 异步 I/O)与 Netty 在功能定位上的边界在哪里?什么场景下应该选择 AIO,什么场景下应该选择 Netty?

  • Java AIO(AsynchronousSocketChannel/AsynchronousFileChannel)与 Netty 的事件驱动模型差异
  • 三种 I/O 模型(BIO/NIO/AIO)的适用场景
  • Netty 为何在通用网络框架中更受青睐

Java AIO(NIO.2)通过 AsynchronousSocketChannel、AsynchronousFileChannel 等提供真正的异步 I/O,由操作系统内核在 I/O 完成时回调,底层在 Linux 上通常基于 epoll 实现,在 Windows 上基于 IOCP 实现,属于"异步非阻塞"模型。Netty 则是一个基于 NIO(同步非阻塞 + Selector 多路复用)的事件驱动网络框架。两者的边界在于:AIO 是 JDK 提供的底层异步 I/O 能力,适合文件异步读写等场景;Netty 是在 NIO 之上封装出的完整网络框架,提供编解码、心跳、粘包拆包、线程池、背压等开箱即用的组件。在 Linux 上,AIO 的 epoll 实现并不比 NIO 的 Selector 有本质性能优势,且 Java AIO 的异常处理与回调使用不便,因此绝大多数的网络服务器(如各网关、中间件)都选择 Netty 而非原生 AIO。Netty 官方也明确表示不推荐在 Linux 上使用 AIO,因为其底层仍是 epoll,性能与 NIO 相当。

理解边界的关键是"异步"与"非阻塞"的区分。AIO 是内核级别完成后回调(异步),NIO 是应用层轮询就绪(非阻塞但同步)。Netty 选择 NIO 而非 AIO,是因为在 Linux 上两者性能相近,而 NIO 的模型更可控、更利于封装。因此 AIO 的适用场景主要是文件异步读写(AsynchronousFileChannel),网络并发场景通常选择 Netty。

#
★★★

2. AIO 在 JDK 25 虚拟线程下的协作

AIO(NIO.2 异步 I/O)在 JDK 25 虚拟线程(Virtual Threads)成熟之后,两者如何协作?虚拟线程是否会取代 AIO?

  • 虚拟线程(JEP 444/425)与 AIO 的定位差异
  • 虚拟线程如何解决阻塞式 I/O 的线程开销问题
  • JDK 25 中 AIO 与虚拟线程共存的实际意义

虚拟线程(Virtual Threads)是 JDK 19 引入、JDK 21 正式发布的轻量级线程,它允许开发者用传统的阻塞式编程模型(如普通的 Socket I/O)编写高并发代码,而不会像平台线程那样消耗大量系统资源。虚拟线程的核心价值在于"用阻塞式代码获得并发性能",它把阻塞的线程调度到极少量的 carrier 线程上,从而支持成千上万甚至百万级别的并发连接。AIO 的异步回调模型在虚拟线程出现后其必要性大幅降低,因为开发者可以用虚拟线程 + 阻塞式 I/O 获得同样的并发能力,而代码更简单、更易读。在 JDK 25 中,AIO 仍然存在并被支持,但官方建议对大多数网络场景优先采用虚拟线程 + 阻塞式 I/O 或 NIO;AIO 主要保留用于文件异步读写等特殊场景。两者并非二选一,而是针对不同瓶颈的解决方案:虚拟线程解决了"线程数量"问题,AIO 解决了"I/O 完成通知"问题。

虚拟线程与 AIO 解决的是不同层面的问题。虚拟线程让阻塞式 I/O 变得廉价,从而简化为每个连接分配一个线程;AIO 则让回调式异步 I/O 成为可能。JDK 25 中虚拟线程的成熟使得传统 AIO 模型在服务端网络编程中的竞争力下降,但文件异步 I/O 等场景仍可用 AIO。这是理解"阻塞式模型回归"趋势的关键。

#
★★★

3. AIO(NIO.2 异步)模型与回调/Future

Java AIO(NIO.2 异步 I/O)模型是如何工作的?它提供回调(CompletionHandler)与 Future 两种编程方式,这两种方式有何区别?

  • AIO 的异步非阻塞模型与 CompletionHandler 回调
  • Future 模式与回调模式的差异
  • AIO 在服务端(AsynchronousServerSocketChannel)的应用

AIO(NIO.2)提供了真正的异步 I/O:当调用读写方法时立即返回,底层 I/O 完成后由内核通过回调机制通知。编程方式有两种:一是传入 CompletionHandler 回调,I/O 完成时调用 completed(),失败时调用 failed();二是使用同步返回的 Future 对象,通过 Future.get() 阻塞等待结果。核心 API 包括 AsynchronousSocketChannel(异步客户端通道)、AsynchronousServerSocketChannel(异步服务端通道)和 AsynchronousFileChannel(异步文件通道)。回调方式更贴近异步模型,适合事件驱动的高并发场景;Future 方式则允许在需要时阻塞等待,便于与已有的同步代码整合。AIO 底层由操作系统提供真正的异步能力,在 Windows 上基于 IOCP,在 Linux 上基于 epoll(模拟异步)。

AIO 与 NIO 的本质区别在于:NIO 是"同步非阻塞",应用层需要自己轮询事件就绪;AIO 是"异步非阻塞",内核完成 I/O 后主动通知。回调与 Future 是暴露异步结果的两套 API,Future 偏同步、便于组合,CompletionHandler 偏异步、流式。理解这一模型是掌握 AIO 的基础。

#
★★★

4. BIO(Blocking I/O)的线程-连接 1:1 模型在 C10K 问题下的资源瓶颈

BIO(Blocking I/O)采用"一个线程对应一个连接"(1:1)模型,在 C10K(一万并发连接)问题下会面临哪些资源瓶颈?

  • BIO 的线程-连接 1:1 模型
  • 线程资源(内存、上下文切换)的限制
  • C10K 问题的本质与 NIO 的改进

BIO 为每个连接分配一个独立的线程,父线程 accept 到一个连接后就创建一个新线程去处理。这种 1:1 模型在连接数达到 C10K(一万到十万级别)时会出现严重瓶颈:首先,每个线程默认栈空间约 1MB(JDK 17+ 默认 1MB),一万个线程就需要约 10GB 内存用于栈,内存开销巨大;其次,大量线程并发时 CPU 上下文切换开销呈指数级增长,可用线程数受限于操作系统与 CPU 核数;再次,多数连接处于空闲等待状态(阻塞在 read),浪费大量线程资源。这就是 C10K 问题的根源。解决方案是 NIO 的非阻塞 + Selector 多路复用,用少量线程管理大量连接,以及虚拟线程让阻塞式 I/O 的线程成本变得廉价。

C10K 的本质是"线程数与连接数成正比"导致的资源耗尽。BIO 的 1:1 模型把线程当作稀缺资源,而大量连接并不总是活跃,导致资源浪费。NIO 的 Selector 让一个线程可以监控多条通道,虚拟线程则让每个连接一个线程的成本变得可接受。理解这一资源瓶颈是理解 NIO 出现动机的核心。

#
★★★

5. BIO(同步阻塞)模型与典型 one-connection-per-thread 模式

BIO(同步阻塞 I/O)模型的工作原理是什么?它典型的 one-connection-per-thread(每连接一线程)模式是如何实现的?

  • BIO 同步阻塞的本质
  • one-connection-per-thread 模式的实现
  • BIO 的优缺点与适用场景

BIO(Blocking I/O)是同步阻塞 I/O:当线程调用 accept()、read()、write() 时,如果数据未就绪,线程会一直阻塞,直到操作完成。其典型实现是 one-connection-per-thread 模式:主线程循环调用 accept() 接受连接,每接受一个连接就创建一个新的工作线程专门处理该连接,工作线程在连接上阻塞等待读写。这种模式的优点是编程简单、模型直观、易于理解与调试;缺点是线程数与连接数成正比,连接量大时线程资源耗尽,且线程在等待时浪费资源。适用场景是连接数少、且每个连接都有活跃读写的场景,例如少量客户端的长连接。由于 JDK 21+ 虚拟线程的成熟,用虚拟线程实现 one-connection-per-thread 模式可以同时获得简单性与高并发能力。

one-connection-per-thread 是 BIO 最直观的实现方式,核心是"每个连接一个线程"的隔离,一个连接阻塞不影响其他连接。其缺陷在于线程池的规模与连接数强耦合。虚拟线程的出现使这种模式重新流行,因为线程成本大幅降低。理解这一模式是面试 BIO 的基础。

#
★★★

6. Buffer 的核心属性(capacity/position/limit/mark)

Java NIO Buffer 的四个核心属性 capacity、position、limit、mark 分别是什么含义?它们如何决定 Buffer 的读写边界?

  • Buffer 的 capacity/position/limit/mark 四属性
  • 读写模式切换(flip/rewind/clear/compact)
  • mark 与 reset 的配合

Java NIO Buffer 是一个用于存储数据的容器,它有四个核心属性:capacity(容量)表示 Buffer 最多能容纳的元素数量,创建后不可改变;position(位置)表示下一次读写操作的索引,随读写推进而增加;limit(限制)表示可读/可写元素的边界,在写模式下等于 capacity,在 flip 之后变为已写入的数据量;mark(标记)是一个可选的记忆位置,用于配合 reset() 返回。关键方法:flip() 将写模式切换为读模式,把 limit 设为 position、position 归零;clear() 为读转写做准备,将 limit 设为 capacity、position 归零(但不清空数据,以覆盖方式写入);rewind() 仅将 position 归零用于重新读取;compact() 将未读数据搬移到开头便于继续写。mark 与 reset() 配合可以在读取过程中记住位置并回退。

Buffer 四属性是 NIO 的基石。position 与 limit 构成读写窗口,flip 负责在读写模式间切换,mark 提供回退能力。理解这些属性与方法的配合,才能正确使用 ByteBuffer 等各类 Buffer,避免 position/limit 出错导致的读写错误。

ByteBuffer buf = ByteBuffer.allocate(16);
buf.put("hello".getBytes());   // position=5, limit=16
buf.flip();                    // position=0, limit=5
byte[] out = new byte[buf.remaining()];
buf.get(out);                  // 读取 5 字节
buf.clear();                   // 复位,position=0, limit=16
#
★★★

7. CharBuffer/IntBuffer 的工程应用

CharBuffer 和 IntBuffer 在工程实践中有哪些典型应用?它们与 ByteBuffer 有何区别?

  • CharBuffer 与字符处理、字符串转换
  • IntBuffer 与整型数据批处理
  • 各类 Buffer 的视图与类型化访问

CharBuffer 是字符类型的 Buffer,用于处理字符序列,常见应用包括:将字符串装入 CharBuffer 进行字符编码(配合 CharsetEncoder)、通过 CharBuffer 操作大量字符而避免频繁创建 String、以及实现字符流的批处理。IntBuffer 是整数类型的 Buffer,用于批处理整型数据,例如图形处理中的像素数据、数据采集中的整数数组,可以直接通过 asIntBuffer() 从 ByteBuffer 创建视图而无需逐元素转换。CharBuffer 与 IntBuffer 都是类型化 Buffer,继承自 Buffer,通过 ByteBuffer.asCharBuffer()/asIntBuffer() 可以创建视图,共享底层字节但按类型解释。与 ByteBuffer 相比,类型化 Buffer 提供了按类型读写的方法(如 put/get char、int),省去手动字节转换,但不改变底层存储。

CharBuffer 与 IntBuffer 的价值在于"类型化访问"与"视图复用"。它们建立在 ByteBuffer 之上,允许按字符或整数语义读写,减少手动转换。CharBuffer 尤其适合字符编码与解码场景,可配合 CharsetEncoder 使用。理解它们是掌握 Buffer 类型体系的关键。

#
★★★

8. CompletionStage 与 AIO 的整合

CompletionStage(CompletableFuture)与 Java AIO 如何整合?AIO 的异步结果如何桥接到 CompletableFuture 链式编程?

  • CompletableFuture 的异步编排能力
  • AIO 的 CompletionHandler 回调如何转换为 CompletableFuture
  • 异步 I/O 与函数式编程的结合

CompletionStage 是 Java 8 引入的异步编排抽象,CompletableFuture 是其实现,提供了 thenApply、thenCompose、exceptionally 等链式组合方法。Java AIO 的异步操作通过 CompletionHandler 回调或 Future 返回结果。将两者整合的核心思路是:创建一个 CompletableFuture,在 AIO 的 CompletionHandler 的 completed() 中调用 future.complete(result),在 failed() 中调用 future.completeExceptionally(ex),从而把 AIO 的回调式异步结果转化为 CompletableFuture,之后就可以用函数式链式编排来处理结果或异常。Future 方式也可以直接桥接:由于 AIO 返回的 Future 本身就是 java.util.concurrent.Future,可以结合 CompletableFuture.supplyAsync + future.get() 包装,但会引入阻塞。更优雅的方式是自定义实现,把回调封装进 CompletableFuture,从而获得非阻塞的异步编排能力。

整合的关键是模式转换:把"回调式"异步结果封装为"CompletableFuture"对象,从而享受链式编排与异常处理。这避免了回调地狱,也让 AIO 与虚拟线程、CompletableFuture 生态协同。理解 complete/completeExceptionally 的桥接是核心。

CompletableFuture<Integer> cf = new CompletableFuture<>();
channel.read(buffer, cf, new CompletionHandler<Integer, CompletableFuture<Integer>>() {
    public void completed(Integer result, CompletableFuture<Integer> attachment) {
        attachment.complete(result);
    }
    public void failed(Throwable exc, CompletableFuture<Integer> attachment) {
        attachment.completeExceptionally(exc);
    }
});
cf.thenAccept(n -> System.out.println("read " + n + " bytes"));
#
★★★

9. EpollSelectorImpl 在 Linux 6.x 内核下的 EPOLLEXCLUSIVE 模式是否被 JDK NIO 启用

JDK NIO 的 EpollSelectorImpl 在 Linux 6.x 内核下是否启用了 EPOLLEXCLUSIVE 模式?该模式的作用是什么?

  • epoll 的 EPOLLEXCLUSIVE 标志与惊群效应(thundering herd)
  • JDK NIO EpollSelectorImpl 的实现
  • 多线程 accept 时的唤醒策略

EPOLLEXCLUSIVE 是 Linux 内核 4.5 引入的 epoll 标志,用于解决"惊群效应"(thundering herd)问题:当多个线程/进程用 epoll 等待同一事件时,默认所有等待者都会被唤醒,但只有一个能处理,造成浪费;EPOLLEXCLUSIVE 确保只唤醒一个等待者。JDK 的 EpollSelectorImpl 在 Linux 上通过 epoll_create、epoll_ctl、epoll_wait 实现多路复用。是否启用 EPOLLEXCLUSIVE 取决于 JDK 版本与具体实现:JDK 在较新版本中针对某些场景(如多个 Selector 或系统属性)支持该标志,但默认的单 Selector 多线程 accept 场景并非直接使用 EPOLLEXCLUSIVE,因为 JDK 通常由单一 Selector 线程负责,不存在多线程同时 epoll_wait 同一 fd 的惊群问题。EPOLLEXCLUSIVE 更常用于 libevent、Netty 等框架在多线程同时管理共享 fd 的场景。JDK 是否启用该标志可查看源码中 EpollArrayWrapper 与系统属性(如 sun.nio.ch 相关)的配置。

EPOLLEXCLUSIVE 的核心价值是消除惊群。JDK NIO 默认单 Selector 的模式天然避免了多线程惊群,因此多数情况下无需 EPOLLEXCLUSIVE。理解该标志需要结合"多线程共享 fd 的 epoll_wait"场景。面试应指出 JDK 主要在单线程 Selector 模型下运行,惊群并不常见。

#
★★★

10. Files.copy/Files.move 在 JDK 25 中的 NIO.2 实现与原子操作语义

Files.copy 和 Files.move 在 JDK 25 中基于 NIO.2 是如何实现的?它们的原子操作语义是什么?

  • Files.copy、Files.move 的 NIO.2 实现
  • StandardCopyOption(ATOMIC_MOVE/REPLACE_EXISTING)的语义
  • 原子操作与文件系统底层实现的关系

Files.copy 和 Files.move 是 NIO.2 提供的文件操作便捷方法,底层通过 FileSystemProvider 实现,具体到 Unix 平台由 UnixFileSystemProvider 调用 native 系统调用。Files.copy 通常先尝试复制文件内容再复制属性,可指定 REPLACE_EXISTING 覆盖、COPY_ATTRIBUTES 复制属性、NOFOLLOW_LINKS 不跟随符号链接。Files.move 在不同情况下有不同实现:若源与目标在同一文件系统(同一挂载点),会尝试使用 rename() 系统调用,这是原子操作,可指定 ATOMIC_MOVE 强制要求原子移动(若底层不支持则抛 AtomicMoveNotSupportedException);若跨文件系统,则退化为"复制+删除"(copy 后 delete),此时不是原子的。REPLACE_EXISTING 在 move 时也参与判断。理解原子性必须区分"同一文件系统"与"跨文件系统"。

原子操作语义是本题核心。rename 在同一文件系统内是原子的(内核保证),跨文件系统不可能原子,只能复制+删除。ATOMIC_MOVE 选项显式要求原子性,若底层不支持则抛异常。面试需指出 move 的原子性取决于是否在同一文件系统。

#
★★★

11. Future 模式在 AIO 的应用

Future 模式在 Java AIO 中是如何应用的?AIO 的异步操作返回的 Future 与普通 Future 有何区别?

  • AIO 异步操作返回的 Future 对象
  • Future.get() 的阻塞等待语义
  • 与 CompletionHandler 回调的对比

Java AIO 的异步读写操作(如 AsynchronousSocketChannel.read()、AsynchronousFileChannel.write())在调用时立即返回,如果传入 null 作为 CompletionHandler,则返回一个 java.util.concurrent.Future 对象,调用者可以通过 Future.isDone() 检查是否完成、通过 Future.get() 阻塞等待结果(返回实际读写的字节数)。这种 Future 模式与回调模式是 AIO 并行的两种结果获取方式。与普通 Future 相比,AIO 返回的 Future 由内部异步 I/O 完成时设置结果,其 get() 会阻塞当前线程直到 I/O 完成,适合在需要同步等待结果的场景使用;而 CompletionHandler 则完全非阻塞,I/O 完成时回调。Future 模式便于与已有的同步代码、ExecutorService 模式整合,但会阻塞线程,不适合高并发纯异步场景。

Future 模式在 AIO 中提供"按需等待"的能力。它把异步操作包装成可查询完成状态、可阻塞获取结果的对象。与回调模式的核心区别是"是否阻塞获取结果"。理解 get() 的阻塞语义与回调的差异是本题关键。

#
★★★

12. JDK 25 虚拟线程与 Selector 阻塞(select pinning)问题的现状与官方建议

JDK 25 中虚拟线程与 NIO Selector 的阻塞操作之间存在"pinning"问题,其现状如何?官方建议是什么?

  • 虚拟线程的 pinning(钉住)问题
  • Selector 阻塞操作与虚拟线程的兼容性
  • JDK 官方建议与解决方案

虚拟线程的 pinning 问题指的是:当虚拟线程在某个不支持空转(yield)的阻塞操作中阻塞时,它会被"钉住"(pinned)在其 carrier 线程(平台线程)上,导致该 carrier 线程无法被其他虚拟线程复用,从而降低吞吐。JDK 中一些 sync 化代码块或 native 阻塞调用(如早期的 Selector 阻塞)可能触发 pinning。JDK 24 引入了 JEP 491(Synchronize Virtual Threads without Pinning),使 synchronized 块内的阻塞不再永远钉住虚拟线程。对于 Selector 的 select() 阻塞:JDK 的 Selector 操作默认在虚拟线程上执行时,JVM 会考虑是否可空转。官方建议:若虚拟线程阻塞在 Selector.select() 时出现 pinning 导致 carrier 线程被占用,应避免在虚拟线程内长时间阻塞调用 Selector 的同步方法,或改用阻塞式 Socket(虚拟线程最擅长)而非 Selector 多路复用。虚拟线程与阻塞式 I/O 是推荐组合,Selector 多路复用更适合平台线程模型。

pinning 是虚拟线程与阻塞操作结合时的隐患。Selector 的 select 阻塞若触发 pinning,会占用 carrier 线程。官方建议为:虚拟线程场景优先使用阻塞式 I/O,Selector 多路复用留给平台线程。理解 pinning 的本质与官方倾向是本题关键。

#
★★★

13. Java NIO 2.0 的 Path/Paths/Files 演进

Java NIO 2.0 引入的 Path、Paths、Files 类相比旧的文件 I/O 有哪些演进?它们如何使用?

  • Path 接口与 Paths 工具类
  • Files 类的静态方法
  • NIO.2 相对旧 File 类的改进

NIO 2.0(JDK 7)引入了 Path、Paths、Files 三个核心类,取代旧的 File 类进行文件系统操作。Path 是路径的抽象,表示文件系统中的一个路径,可以表示相对或绝对路径,支持 resolve、relativize、normalize 等路径操作;Paths.get() 是创建 Path 的便捷工厂方法。Files 是静态工具类,提供了大量文件操作:读写(readAllBytes、newBufferedReader、write)、判断(isDirectory、exists)、复制/移动(copy、move)、删除(delete、deleteIfExists)、遍历(walk、list、find)、属性(getAttribute、readAttributes)、符号链接处理等。相比旧 File 类,NIO.2 的改进包括:支持符号链接、文件属性视图(BasicFileAttributeView 等)、与文件系统提供者(FileSystemProvider)解耦、支持 WatchService 文件变更监控、统一的流式 API。Files 的方法通常返回 Path 或 Stream,更符合函数式编程风格。

NIO.2 的演进核心是"以 Path 抽象路径、以 Files 提供操作、以 FileSystemProvider 解耦实现"。它解决了旧 File 类功能单一、无法扩展文件系统类型(如 zip、内存文件系统)的问题。理解 Path/Files 的配合使用是 NIO.2 的基础。

Path p = Paths.get("/tmp/data.txt");
Path resolved = p.resolve("sub/file.txt");      // 路径拼接
byte[] bytes = Files.readAllBytes(p);            // 读取全部
Files.write(p, "hello".getBytes());              // 写入
Files.deleteIfExists(p);                         // 存在则删除
#
★★★

14. Java NIO 的水平触发(Level-Triggered)本质

Java NIO 的 Selector 在 Linux 上采用水平触发(Level-Triggered)模式,其本质是什么?与边缘触发(Edge-Triggered)有何区别?

  • 水平触发(LT)与边缘触发(ET)的区别
  • Java NIO Selector 默认使用水平触发
  • 水平触发对编程模型的影响

水平触发(Level-Triggered)是指:只要 fd(文件描述符)上的某个事件仍然处于"可读/可写"状态,epoll_wait/select 每次都会报告该事件,直到数据被完全读取或缓冲区被写满。边缘触发(Edge-Triggered)则只在事件状态发生变化时报告一次,之后即使数据仍可读也不会再次报告,直到再次发生状态跳跃。Java NIO 的 Selector 在 Linux 上基于 epoll 时默认使用水平触发(EPOLLLT),即只要通道可读或可写,select 就会持续返回就绪事件。水平触发的优点是编程简单、不易丢事件,即使没读完数据下次仍会报告;缺点是可能造成重复唤醒(busy loop),若可读数据一直没读完,select 会一直返回。Java NIO 不暴露 ET 模式,et 模式通常由 Netty 等框架通过原生 API 或 epoll 的 EPOLLET 标志实现。水平触发也意味着开发者每次 select 后应尽量读完就绪数据,避免重复唤醒。

水平触发本质是"只要状态就绪就持续报告",与边缘触发"只在状态变化时报告"相对。Java NIO 默认水平触发,降低了编程复杂度但可能重复唤醒。理解这一本质有助于解释 select 的返回值与 NIO 的编程习惯。

#
★★★

15. 零拷贝(Zero-Copy)在 Java 中的实现(FileChannel.transferTo)

零拷贝(Zero-Copy)在 Java 中如何通过 FileChannel.transferTo 实现?其原理是什么?

  • FileChannel.transferTo 的零拷贝语义
  • sendfile 系统调用与用户态/内核态拷贝
  • 零拷贝减少的拷贝次数

零拷贝(Zero-Copy)是指数据在用户态与内核态之间传输时,尽量减少甚至消除 CPU 拷贝操作。传统的文件传输(read + write)需要四次拷贝:文件读到内核缓冲、内核缓冲拷到用户态、用户态拷到发送缓冲、发送缓冲拷到网卡。FileChannel.transferTo() 在 Linux 上通过 sendfile 系统调用实现,直接将数据从文件在内核的页缓存传输到 socket 发送缓冲,再经网卡发送,全程不需要把数据拷贝到用户态,从而消除了用户态拷贝,要么是两次拷贝(DMA 拷贝 + 内核拷贝),要么借助支持 DMA 的网卡实现真正的单次拷贝。Netty 的 FileRegion 与 Kafka 的日志发送都利用 transferTo 实现零拷贝。需注意 transferTo 在 Windows 上实现不同,且受限于文件系统与 socket 类型,某些平台可能会退化为普通拷贝。

零拷贝的核心是"避免用户态参与数据搬运"。transferTo 借助 sendfile 让内核直接完成文件到 socket 的传输,减少了一次或多次拷贝。理解传统 read/write 与 sendfile 的拷贝路径差异是本题关键。

#
★★

16. AIO 与 io_uring 提案的边界

Java AIO 与 io_uring 提案各是什么?它们之间的边界在哪里?

  • io_uring 作为新一代异步 I/O 系统调用
  • AIO 与 io_uring 在性能与模型上的差异
  • JDK 对 io_uring 的支持现状

io_uring 是 Linux 5.1 引入的新一代异步 I/O 接口,它通过共享内存的环形队列(SQ 提交队列、CQ 完成队列)提交和获取 I/O 请求,减少了系统调用次数,支持真正的异步文件与网络 I/O,性能显著优于传统 epoll 和 libaio。Java AIO(NIO.2)在 Linux 上当前基于 epoll 模拟异步,并未直接使用 io_uring。两者之间的边界是:io_uring 是操作系统层面的、更高效的异步 I/O 原语,而 AIO 是 JDK 提供的 API 抽象。JDK 社区有关于引入 io_uring 作为底层实现的提案(如 JEP 讨论中出现的 io_uring 支持),但截至 JDK 25 尚未正式将 io_uring 作为默认 NIO/AIO 的底层实现,仍主要使用 epoll。Netty 等框架通过原生库(如 netty-incubator-transport-io_uring)可间接使用 io_uring。io_uring 的边界在于:它是底层内核接口,JDK 的 AIO/NIO 是上层 API,两者可通过桥接结合,但需考虑内核版本要求与可移植性。

边界在于"API 抽象"与"内核原语"的关系。AIO 是 JDK 的异步 I/O API,io_uring 是更高效的内核实现。JDK 尚未将 io_uring 作为默认底层,但它是未来高性能 I/O 的方向。理解现状可知 AIO 在 Linux 上仍基于 epoll。

#
★★

17. AIO 在 Linux 上基于 epoll 的实现

Java AIO 在 Linux 上是如何基于 epoll 实现的?这种实现有何特点?

  • AIO 在 Linux 上的底层实现机制
  • epoll 模拟异步的特点
  • 与 Windows IOCP 的对比

Java AIO 在 Linux 上的实现并不是真正依赖内核的异步 I/O(如 io_uring 或 libaio),而是通过 NIO 的 Selector 机制(epoll)模拟异步。具体地,AsynchronousChannel 在 Linux 上由内部线程池配合 epoll 监听套接字事件,当 epoll 报告通道可读/可写时,由内部线程执行实际的读写,完成后触发 CompletionHandler 回调。这种"epoll 模拟异步"的实现使得 AIO 在 Linux 上的性能与 NIO 的 Selector 模型没有本质差异,因为底层仍是基于事件循环的轮询与回调。这与 Windows 上 AIO 基于真正的 IOCP(I/O Completion Port)不同,IOCP 是内核级别的真正异步,读写请求提交后由内核完成并发送完成通知。因此,在 Linux 上使用 Java AIO 与使用 NIO 在性能上相近,这也是 Netty 等框架在 Linux 上不推荐 AIO 的原因之一。

关键点是 Linux 上 AIO 用 epoll 模拟异步,并非真异步。因此性能与 NIO 相当。Windows 上 IOCP 才是真正的异步。理解这一差异可解释为何 AIO 在 Linux 上未获普及。

#
★★

18. AIO 的 AsynchronousChannelGroup 线程池如何配置,回调线程与业务线程池如何避免相互阻塞

AIO 的 AsynchronousChannelGroup 线程池应当如何配置?回调线程与业务线程池如何避免相互阻塞?

  • AsynchronousChannelGroup 的作用与线程池配置
  • 回调线程与业务线程池的职责划分
  • 避免阻塞的实践经验

AsynchronousChannelGroup 是分组管理一组异步通道和相关线程池的机制,通道共享该组的线程池。可通过 AsynchronousChannelGroup.withThreadPool(ExecutorService) 或 withFixedThreadPool 来配置线程池。线程池中的线程负责执行 I/O 完成后的回调(CompletionHandler)。关键设计是避免回调线程与业务线程池相互阻塞:回调线程只负责把结果交给业务线程池,不要在回调线程中执行耗时业务逻辑(如数据库访问、远程调用),否则会阻塞 I/O 完成线程,拖慢整个异步通道的处理。正确做法是:回调线程中快速完成结果传递,通过业务线程池(如独立 ExecutorService)异步执行重逻辑,使 I/O 回调线程保持空闲以处理后续完成事件。此外,应避免在回调中调用阻塞方法,回调线程池大小应合理配置,避免线程耗尽。

核心是"职责分离":I/O 回调线程只做轻量结果传递,业务重逻辑交给业务线程池。若两者共用线程池或回调中阻塞,会造成线程饥饿与相互阻塞。合理配置线程池并分离职责是高性能异步 I/O 的关键。

#
★★

19. ByteBuffer 与 Netty ByteBuf 的差异

JDK 的 ByteBuffer 与 Netty 的 ByteBuf 有哪些主要差异?Netty 为什么设计自己的 ByteBuf?

  • ByteBuffer 与 ByteBuf 的 API 差异
  • 读写指针、动态扩容、池化等特性
  • Netty 设计 ByteBuf 的动机

ByteBuffer 是 JDK NIO 的缓冲区,使用单一的 position 指针,读写模式切换需要调用 flip/rewind/clear,且不支持动态扩容,容量固定。Netty 的 ByteBuf 则采用独立的 readerIndex 和 writerIndex 两个指针,读写互不干扰,无需 flip 切换;支持动态扩容(write 时自动增长);通过 reference-counting 引用计数管理内存;支持池化(PooledByteBufAllocator)以复用内存,减少 GC 与分配开销;支持 CompositeByteBuf 组合多个缓冲区实现零拷贝;还提供丰富的 API(如 readBytes、writeInt、slice、duplicate)。Netty 设计自己的 ByteBuf 是为了解决 ByteBuffer 的痛点:单一指针导致读写切换繁琐、无法动态扩容、无池化、无引用计数、不利于高性能网络框架的内存管理。ByteBuf 更适合网络协议编解码场景。

差异核心是"双指针 + 动态扩容 + 池化 + 引用计数"。ByteBuffer 的单一 position 与固定容量在复杂网络编程中不便,ByteBuf 通过双指针和池化大幅提升易用性与性能。理解这些差异是面试 Netty 的常见考点。

#
★★

20. ByteBuffer 复用是否带来显著差异

复用 ByteBuffer 相比每次新建 ByteBuffer 是否带来显著性能差异?为什么?

  • 对象分配与 GC 开销
  • 直接缓冲区与堆缓冲区复用的差异
  • 池化思想

复用 ByteBuffer 通常能带来显著的性能提升,尤其是在高并发、高吞吐的 I/O 场景下。原因在于:每次新建 ByteBuffer 都会产生对象分配与 GC 压力,若是直接缓冲区(DirectByteBuffer),分配和释放还涉及操作系统内存与 Cleaner 清理,开销更大。复用 ByteBuffer 可以避免频繁分配,减少 GC 停顿,提高吞吐。对于直接缓冲区,复用还避免了频繁的系统内存分配与页表映射。不过复用也有代价:需要开发者自行管理生命周期,注意复位(clear/rewind)与数据同步,避免数据串扰;多个线程共享同一 ByteBuffer 时需同步。Netty 通过池化(PooledByteBufAllocator)实现了 ByteBuffer 的自动复用,本质上是把"复用"交给框架管理。总体而言,在高频 I/O 场景下复用或池化能带来显著差异,但需权衡正确性与复杂度。

复用的收益主要来自减少分配与 GC,直接缓冲区尤其明显。但复用引入生命周期管理复杂度与线程安全风险。在低频场景差异不明显,高频 I/O 下差异显著。Netty 的池化是复用思想的工程化。

#
★★

21. ByteBuffer 的 wrap 工厂

ByteBuffer.wrap 工厂方法的作用是什么?它与其他分配方式有何区别?

  • ByteBuffer.wrap 的语义
  • 包装数组与直接分配的区别
  • 共享底层数组的特性

ByteBuffer.wrap(byte[]) 是一个静态工厂方法,它用一个已有的字节数组创建一个 ByteBuffer,该 buffer 直接以传入的数组为底层存储,不做拷贝。因此对 buffer 的修改会反映到原数组,对原数组的修改也会反映到 buffer,二者共享同一份数据。wrap 还可指定 offset 和 length,限制 buffer 的读写范围。与其他分配方式(ByteBuffer.allocate 创建新的堆缓冲区、ByteBuffer.allocateDirect 创建直接缓冲区)相比,wrap 不分配新内存,只是复用已有数组,适合将已有的字节数组包装为 Buffer 以便使用 NIO API,例如将网络接收到的字节数组包装后读取。注意 wrap 创建的 buffer 是堆内缓冲区,读写受数组大小限制,且依赖外部数组的生命周期。

wrap 的核心是"零拷贝复用已有数组"。它不分配新内存,只是建立视图。理解 wrap 与 allocate 的区别(是否新分配、是否共享底层)是掌握 Buffer 创建方式的关键。

byte[] data = "hello".getBytes();
ByteBuffer buf = ByteBuffer.wrap(data);   // 复用 data,不拷贝
buf.put(0, (byte) 'H');                   // 修改会反映到 data
#
★★

22. ByteBuffer 的分散/聚集 I/O(ScatteringByteChannel)

什么是分散/聚集 I/O(Scatter/Gather)?Java 中如何通过 ScatteringByteChannel 实现?

  • 分散读取与聚集写入
  • ScatteringByteChannel/GatheringByteChannel 接口
  • 一次系统调用读写多个缓冲区

分散/聚集 I/O(Scatter/Gather)指的是:一次系统调用可以从一个通道读取数据到多个缓冲区(分散读取,scatter),或从多个缓冲区写入数据到一个通道(聚集写入,gather)。Java 中通过 ScatteringByteChannel 接口的 read(ByteBuffer[]) 实现分散读,通过 GatheringByteChannel 接口的 write(ByteBuffer[]) 实现聚集写。其底层会调用 readv/writev 系统调用,一次调用处理多个缓冲区,从而减少系统调用次数,提高 I/O 效率。典型应用是协议处理:协议头和数据体分别放在不同缓冲区,一次读取同时填充;或发送时把多个数据块(如协议头 + 载荷)一次写入。ByteBuffer[] 数组中的每个 buffer 按顺序被填充或读出,直到数据耗尽或缓冲区填满。

分散/聚集的核心是一次系统调用处理多个缓冲区,减少系统调用开销。它常与协议解析、零拷贝结合。理解 readv/writev 与 ScatteringByteChannel 的配合是本题关键。

ScatteringByteChannel ch = ...;
ByteBuffer head = ByteBuffer.allocate(4);
ByteBuffer body = ByteBuffer.allocate(1024);
ch.read(new ByteBuffer[]{head, body});  // 一次读取,先填 head 再填 body
#
★★

23. ByteBuffer 的直接缓冲(Direct)与堆内缓冲

ByteBuffer 的直接缓冲(Direct Buffer)与堆内缓冲(Heap Buffer)有何区别?各自适用什么场景?

  • Direct 与 Heap 缓冲的内存位置
  • 与操作系统 I/O 的关系
  • 性能与内存管理差异

直接缓冲(DirectByteBuffer)通过 ByteBuffer.allocateDirect() 分配,其内存在 JVM 堆之外(堆外内存),由操作系统直接管理,在进行 I/O 时无需在堆与系统内存之间拷贝,因此某些 I/O 场景性能更高。堆内缓冲(HeapByteBuffer)通过 ByteBuffer.allocate() 分配,内存在 JVM 堆中,受 GC 管理,但在进行 I/O 操作时 JVM 通常需要做一次堆内到直接内存的拷贝(因为底层 I/O 需要连续且稳定的内存地址),因此多一次拷贝。直接缓冲的优点是减少 I/O 拷贝、适合高频大块 I/O;缺点是分配/释放开销大(涉及系统内存与 Cleaner 清理)、不受 GC 管理,需要手动或依赖 Cleaner 释放,且不受 -Xmx 堆大小限制(受 MaxDirectMemorySize 限制)。适用场景:网络 I/O、文件 I/O 等需要系统调用的场景,且通常需要复用/池化以摊销分配开销;堆内缓冲适合小数据量、生命周期短、不需要频繁系统调用的场景。

直接缓冲的本质是"堆外内存 + 减少 I/O 拷贝",但分配与回收成本高。堆内缓冲受 GC 管理但 I/O 时有拷贝。理解两者的内存位置与 I/O 路径差异是选择依据。

#
★★

24. ByteBuffer.allocateDirect 的内存回收(Cleaner)

ByteBuffer.allocateDirect 分配的直接缓冲内存是如何回收的?Cleaner 机制是怎样的?

  • DirectByteBuffer 的回收机制
  • Cleaner 与虚引用(PhantomReference)
  • 显式回收与潜在泄漏

DirectByteBuffer 分配的直接内存位于堆外,不受 GC 直接管理,其回收通过两套机制:一是 JVM 的 GC 在对象不可达时,通过关联的 Cleaner(本质是 PhantomReference 虚引用)来回收堆外内存;二是开发者可通过显式手段触发回收。具体地,DirectByteBuffer 内部持有一个 Cleaner 对象,Cleaner 是基于 PhantomReference 实现的,当 ByteBuffer 对象被 GC 判定为不可达后,Cleaner 的清理动作(调用 free() 释放底层的堆外内存)会被调度执行。因此,直接内存的回收依赖 GC 触发,具有不确定性,若未及时 GC 或对象长时间存活,可能造成堆外内存泄漏。开发者应尽量复用直接缓冲(池化),或在明确不再使用时让其尽快成为不可达以触发 GC 回收。长期持有直接缓冲对象且不及时释放,会导致 MaxDirectMemorySize 积压,最终抛 OutOfMemoryError。

直接内存回收依赖 Cleaner(虚引用)与 GC 联动,具有不确定性。频繁分配/不再使用时可能造成堆外内存压力。理解其回收机制与潜在泄漏是管理直接缓冲的关键。

#
★★

25. ByteBuffer.asXxxBuffer 的视图

ByteBuffer.asXxxBuffer 方法的作用是什么?它创建的视图缓冲有何特点?

  • asCharBuffer/asIntBuffer/asLongBuffer 等视图方法
  • 视图共享底层字节
  • 字节序与类型化访问

ByteBuffer.asXxxBuffer() 方法(如 asCharBuffer、asIntBuffer、asLongBuffer、asFloatBuffer、asDoubleBuffer、asShortBuffer)会在 ByteBuffer 之上创建一个类型化视图缓冲,把底层字节按指定类型解释。视图缓冲与原始 ByteBuffer 共享同一段底层数据,对视图的修改会反映到原始 buffer,反之亦然。视图具有独立的 position、limit,但底层存储共享。字节序(ByteOrder)由原始 Buffer 的 order 决定,默认大端。视图缓冲便于按类型读写数据,避免手动进行字节到类型的转换,常用于协议解析、二进制数据处理等场景。注意视图的容量受原始 buffer 剩余字节数限制(按类型宽度整除),且视图不支持直接缓冲区到堆缓冲的特殊性,但可以配合读取。

asXxxBuffer 的核心是"共享底层 + 类型化视图"。它不拷贝数据,只提供不同解释方式。理解视图与底层共享、字节序影响是本题关键。

#
★★

26. DirectByteBuffer 的内存对齐与性能

DirectByteBuffer 的内存对齐为何重要?它与性能有何关系?

  • 内存对齐的概念
  • 零拷贝与 DMA 对对齐的要求
  • DirectByteBuffer 的对齐优化

内存对齐是指数据在内存中的起始地址按一定字节数(如页大小 4KB、或 Cache Line 大小)对齐。DirectByteBuffer 的堆外内存对齐对性能至关重要:某些底层 I/O 操作(如 Redis、Netty 使用的 mmap 或 DMA 传输)要求内存地址对齐,非对齐可能退化为逐字节拷贝或降低 DMA 效率。DirectByteBuffer 分配时通常按页大小对齐(页表对齐),便于内核进行零拷贝、mmap 和 DMA 传输。此外,对齐还能减少缓存行跨越(Cache Line False Sharing),提高存取效率。在 Netty 中,DirectByteBuffer 或 PooledByteBuf 的分配也考虑对齐,以支持零拷贝。内存对齐与零拷贝的关系:对齐的内存地址允许 sendfile、mmap 等直接引用,避免额外的拷贝对齐处理。因此,DirectByteBuffer 的对齐是高性能 I/O 的重要基础。

内存对齐的意义在于适配底层 DMA/零拷贝/缓存行,减少性能退化。DirectByteBuffer 的页对齐分配是为零拷贝与高效 I/O 服务。理解对齐与零拷贝的关联是本题关键。

#
★★

27. FileChannel 在大文件拷贝的应用

FileChannel 在大文件拷贝中有哪些应用?相比普通流拷贝有何优势?

  • FileChannel 的大文件拷贝
  • transferTo/transferFrom 与零拷贝
  • 与普通 InputStream/OutputStream 拷贝对比

FileChannel 提供了高效的大文件拷贝能力,核心是 transferTo() 和 transferFrom() 方法。在 Linux 上,transferTo/transferFrom 底层使用 sendfile 系统调用,实现零拷贝,数据在内核中直接传输,无需在用户态拷贝。即使文件通道与 socket 通道、或两个文件通道之间,也能减少拷贝次数。相比通过 InputStream/OutputStream 循环读写(read + write,涉及多次用户态拷贝与大量系统调用),FileChannel 的 transferTo/transferFrom 一趟完成拷贝,性能显著提升,尤其适合大文件。此外,FileChannel 提供 map() 内存映射可以映射大文件到内存,便于随机访问。使用 FileChannel 拷贝大文件时要注意:transferTo 单次有 2GB 上限(某些平台),需循环调用;应处理返回的已传输字节数,未传完则继续。大文件拷贝是 transferTo 的典型应用场景。

FileChannel 大文件拷贝的优势是零拷贝(sendfile)与减少系统调用,相比普通流拷贝显著提升性能。理解 transferTo 的循环调用与 2GB 限制是实践要点。

#
★★

28. FileChannel 的读写与 transferTo/transferFrom

FileChannel 的读写方法与 transferTo/transferFrom 有何区别?各适用什么场景?

  • FileChannel.read/write 与 transferTo/transferFrom
  • 零拷贝与普通拷贝
  • 适用场景差异

FileChannel.read(ByteBuffer)/write(ByteBuffer) 是常规的通道读写,数据需要经过用户态缓冲区(ByteBuffer),即数据从文件读入内核缓冲,再拷贝到用户态 ByteBuffer,写入时反向拷贝。这种读写适合需要处理数据内容的场景(如解析、加工)。transferTo()/transferFrom() 则直接在两通道之间传输数据,在 Linux 上通过 sendfile 实现零拷贝,数据在内核中直接搬运,不经过用户态,适合"不处理内容、直接搬运"的场景,如文件复制到 socket、两个文件之间的拷贝。区别在于:transferTo/transferFrom 是通道间的零拷贝传输,性能更高但无法修改数据;read/write 是经过用户态缓冲的读写,可处理数据但多一次拷贝。选择依据是"是否需要处理数据内容":只需搬运则用 transferTo/transferFrom,需要解析/加工则用 read/write。

核心区别是"是否经过用户态"。transferTo/transferFrom 零拷贝、不经过用户态,适合纯搬运;read/write 经过用户态,可处理数据。理解这一取舍是选择 I/O 方式的关键。

#
★★

29. FileChannel.force() 与持久化

FileChannel.force() 的作用是什么?它与持久化有何关系?

  • force() 将缓冲数据强制写入磁盘
  • force(true) 是否同步元数据
  • 与普通 write 的区别

FileChannel.force(boolean metaData) 会将通道中尚未持久化的数据(包括操作系统页缓存中的数据)强制写回磁盘,以确保数据持久化。其中参数 metaData 表示是否同时强制更新文件元数据(如修改时间、文件大小等)。普通 write() 只是把数据写入操作系统页缓存,随后由操作系统在适当时候刷盘,若系统崩溃或断电,未刷盘的数据可能丢失。force() 相当于 fsync 系统调用,确保数据真正落盘,是数据持久化的关键。在数据库、日志系统等对数据安全要求高的场景,写入后通常调用 force() 保证持久化。但 force() 会同步等待磁盘 I/O 完成,性能开销较大,因此需要权衡持久化与性能。force(true) 相比 force(false) 更安全但开销更大。

force() 本质是 fsync,把数据从页缓存刷到磁盘,是持久化的保证。普通 write 只进缓存。理解 write 与 force 的差异是数据一致性设计的关键。

#
★★

30. FileChannel.lock 与文件锁

FileChannel.lock 与文件锁的作用是什么?文件锁有哪些类型与限制?

  • FileLock 的共享锁/排他锁
  • lock() 与 tryLock() 的区别
  • 文件锁的跨进程语义与限制

FileChannel.lock() 和 tryLock() 用于对文件加锁,返回 FileLock 对象。锁有两种类型:共享锁(SHARED,读锁)和排他锁(EXCLUSIVE,写锁)。lock() 是阻塞式,调用会阻塞直到获取锁;tryLock() 是非阻塞式,立即返回,若无法获取则返回 null。文件锁是进程级别的(跨 JVM、跨进程),用于协调多个进程对同一文件的访问,而非线程级别的互斥(JVM 内多线程访问同一文件仍需代码同步)。文件锁的语义与操作系统相关:在 Linux 上基于建议锁(advisory lock),其他进程若遵守则有效;在 Windows 上可能是强制锁。注意事项:文件锁依赖于文件系统与操作系统,某些文件系统可能不支持;锁与 FileChannel 关联,通道关闭时锁释放;进程崩溃时操作系统会释放其持有的锁。不能对同一文件在多个 JVM 中重复获取冲突锁。

文件锁是跨进程的协调机制,分共享/排他,lock 阻塞、tryLock 非阻塞。理解其跨进程语义、建议锁/强制锁差异与崩溃释放是实践要点。

#
★★

31. FileChannel.map() 的内存映射文件(Memory-Mapped File)在随机读写场景的性能取舍

FileChannel.map() 创建的内存映射文件(Memory-Mapped File)在随机读写场景下有哪些性能取舍?

  • map() 与 MappedByteBuffer 的原理
  • 内存映射在随机读写的性能优势
  • 内存映射的代价与限制

FileChannel.map() 将文件区域映射到进程地址空间,返回 MappedByteBuffer,之后对缓冲区的读写直接映射到文件,由操作系统页缓存管理,无需显式 read/write 系统调用。在随机读写场景下,内存映射的优势明显:访问文件任意位置只需通过指针定位,操作系统按需加载页到内存,避免了频繁的系统调用,随机访问性能显著优于传统 read/write 的 seek+read。因此数据库、RocketMQ 的 CommitLog 等场景使用 mmap 实现高效顺序与随机读写。但内存映射也有代价:映射需要占用进程地址空间,映射后对文件的修改不立即持久化,需依赖操作系统刷盘或调用 force();映射文件大小受地址空间限制(32 位受限,64 位约协议上限);并发写同一映射区域需要同步;映射脏页的刷盘时机由操作系统控制,崩溃时可能丢失数据。此外,MappedByteBuffer 的回收依赖 GC,频繁映射/取消映射有开销。因此内存映射适合"大文件、随机访问、读多写少"的场景,需要权衡映射生命周期与持久化。

内存映射的取舍核心是"以虚拟内存换取系统调用开销"。随机访问时指针定位快,但持久化需主动 force、地址空间受限、回收依赖 GC。理解其优势(随机访问性能)与代价(持久化、地址空间、回收)是本题关键。

#
★★

32. JDK 25 中虚拟线程对 Selector 的 select/wakeup 调用阻塞的兼容性边界

JDK 25 中虚拟线程对 NIO Selector 的 select/wakeup 调用阻塞有哪些兼容性边界?

  • 虚拟线程中调用 Selector.select/wakeup 的行为
  • 阻塞与 pinning 的兼容性
  • 边界与官方建议

在 JDK 25 中,虚拟线程可以调用 Selector 的 select/selectNow/wakeup 等方法,但存在兼容性边界。select() 是阻塞调用,当虚拟线程执行 select() 阻塞时,JVM 需要考虑是否能把该虚拟线程空转(yield)让出 carrier 线程。早期 JDK 版本中,Selector 的阻塞在某些实现下会触发 pinning(虚拟线程被钉住 carrier 线程),降低并发能力。JDK 24 通过 JEP 491 等改进减少 synchronized 阻塞导致的 pinning,但 Selector 内部基于 native 的等待仍可能受限。边界在于:虚拟线程长时间阻塞在 Selector.select() 上时,可能占用 carrier 线程,影响其他虚拟线程调度。官方建议是:虚拟线程场景优先使用阻塞式 Socket I/O(每连接一个虚拟线程),而不是 Selector 多路复用;Selector 多路复用更适合平台线程的 EventLoop 模型。若在虚拟线程中使用 Selector,应评估 pinning 影响并限制每 carrier 线程的虚拟线程数。

兼容性边界核心是"虚拟线程阻塞在 Selector 上可能 pinning carrier 线程"。虚拟线程天然适配阻塞式 I/O,Selector 多路复用并非其主要场景。理解官方倾向(虚拟线程用阻塞式 I/O)是本题关键。

#
★★

33. JDK 25 虚拟线程下 NIO Selector 的 wakeup 唤醒机制是否仍使用 Pipe 与 Selector.wakeup 内部竞争

JDK 25 虚拟线程下,NIO Selector 的 wakeup 唤醒机制是否仍使用 Pipe?Selector.wakeup 内部是否存在竞争?

  • Selector.wakeup 的实现机制
  • Pipe(管道)与 self-pipe trick
  • 虚拟线程下的内部竞争

NIO Selector 的 wakeup() 用于唤醒阻塞在 select() 的线程。传统实现基于 self-pipe trick:Selector 内部维护一个 Pipe(双向管道),wakeup() 时向 Pipe 写入一个字节,使 select 就绪返回;select 处理完后再读取该字节。Linux 上 epoll 实现后期也通过写入唤醒 fd 实现。在 JDK 25 中,这一机制依然保留,Selector.wakeup 仍通过内部管道/唤醒 fd 触发 select 返回。Selector.wakeup 内部存在竞争:wakeup 与 select 之间是异步的,若先 wakeup 后 select,则下一次 select 会立即返回(因为唤醒标志已置位),Java 保证了这种语义(wakeup 的调用可以合并,先 wakeup 后 select 时下一次 select 立即返回)。虚拟线程下,wakeup 仍由该 Selector 的注册线程或调用线程触发,机制不变,但需注意虚拟线程与平台线程共享 Selector 时的并发安全。整体上,wakeup 机制在 JDK 25 中仍基于 Pipe/唤醒 fd,存在"唤醒标志合并"的语义。

关键点是 wakeup 仍基于 Pipe/self-pipe trick,且具备"可合并、先 wakeup 后 select 下一次立即返回"的语义。虚拟线程不改变这一机制。理解唤醒机制与竞争语义是本题核心。

#
★★

34. Java NIO 在高并发下的边界(epoll 空轮询 bug)

Java NIO 在高并发下有哪些边界问题?epoll 空轮询 bug 是什么?

  • epoll 空轮询 bug 的现象与原因
  • 高并发下的 NIO 边界
  • Netty 的解决方案

Java NIO 在高并发下有一个著名的边界问题:epoll 空轮询 bug。现象是:当文件描述符较多、负载较高时,Linux 的 epoll_wait 可能在没有实际事件的情况下立即返回(返回 0 或可读事件提前触发),导致 Selector 循环在没有就绪事件时不断空转,CPU 占用飙升到 100%,且 select 一直返回,造成死循环。这本质上是 Linux 内核 epoll 在特定场景下的缺陷(与 fd 数量、事件密集度有关)。Netty 通过以下方式解决:在 Selector 循环中统计 select 返回次数,若在短时间内连续多次空轮询(如 selector 每 100ms 空转次数超过阈值),则重建 Selector(新建一个 Selector 并把所有已经注册的 Channel 重新注册迁移到新 Selector),从而避免空转死循环。此外,Java NIO 在高并发下的边界还包括:单 Selector 线程处理能力有限、fd 数量限制、事件处理耗时导致延迟等。

epoll 空轮询 bug 是高并发 NIO 的典型问题,表现为 CPU 100% 死循环。Netty 通过"空转检测 + 重建 Selector"规避。理解该 bug 的现象与 Netty 的解决方式是本题关键。

#
★★

35. Java Selector 的就绪语义与 Linux epoll、macOS kqueue、Windows 实现差异应如何隔离测试

Java Selector 的就绪语义在 Linux epoll、macOS kqueue、Windows 上实现差异如何?应如何隔离测试这些差异?

  • 各平台 Selector 实现的就绪语义差异
  • 可移植性测试与隔离
  • 跨平台行为差异

Java Selector 在不同平台由不同的系统实现支撑:Linux 用 epoll、macOS 用 kqueue、Windows 用 select(Winsock)或 IOCP 相关机制。它们的就绪语义存在差异:epoll 和 kqueue 是事件驱动,能报告可读/可写/连接等事件;Windows 的 select 实现就绪语义与 Unix 不同,且对某些操作(如非阻塞 accept 的返回)行为有差异。差异还体现在:唤醒语义、就绪事件是否持续报告(水平触发 vs 边缘触发)、OP_ACCEPT 的触发时机、wakeup 行为等。测试时应隔离这些差异:通过抽象接口封装 Selector 操作,在测试中用统一的抽象层,编写针对不同平台的条件测试(如按操作系统分支),并利用 CI 在多个平台(Linux/macOS/Windows)上运行测试。还应避免依赖平台特定行为(如 select 返回的时间差异),利用 Selector 的标准化语义(可读/可写/连接就绪)编写与平台无关的断言,并针对平台差异补充专项测试。

隔离测试的核心是"抽象封装 + 平台分支测试 + CI 多平台运行"。就绪语义差异因底层实现(epoll/kqueue/select)而异,测试需不依赖平台特定行为。理解差异来源与测试隔离方法是本题关键。

#
★★

36. Linux epoll 与 JDK NIO Selector 的 epoll_ctl 系统调用映射细节

Linux epoll 与 JDK NIO Selector 的 epoll_ctl 系统调用是如何映射的?有哪些细节?

  • epoll_ctl 的 ADD/MOD/DEL 操作
  • JDK NIO 对 epoll_ctl 的调用
  • 注册与修改 interestOps 的映射

Linux epoll 通过 epoll_ctl 系统调用管理 fd 的事件注册,支持 EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)三种操作。JDK NIO 的 Selector 在 Linux 上由 EpollSelectorImpl 和 EpollArrayWrapper 实现,注册 Channel 时通过 epoll_ctl(EPOLL_CTL_ADD) 将 fd 加入 epoll,修改 interestOps 时通过 EPOLL_CTL_MOD 更新事件掩码,取消注册/关闭时用 EPOLL_CTL_DEL 移除。JDK 内部维护了 epoll 事件数组(EPollArrayWrapper),用位图或数组记录每个 fd 注册的事件,通过 native 方法调用 epoll_ctl。细节:JDK 对事件掩码做了映射(如 OP_READ 映射到 EPOLLIN,OP_WRITE 到 EPOLLOUT,OP_CONNECT 到 EPOLLOUT,OP_ACCEPT 到 EPOLLIN);epoll_ctl 每次调用都涉及一次系统调用,频繁修改 interestOps 会带来系统调用开销,因此 Netty 等框架会合并/延迟 interestOps 更新以减少系统调用。理解 epoll_ctl 的 ADD/MOD/DEL 与 JDK 的映射是掌握 NIO 底层的关键。

映射核心是"interestOps 到 epoll 事件掩码 + epoll_ctl 的 ADD/MOD/DEL"。JDK 通过 EpollArrayWrapper 调用 epoll_ctl。频繁修改 interestOps 有系统调用开销,是 Netty 合并更新的动机。理解映射细节是本题关键。

#
★★

37. Linux epoll 的 LT/ET 模式与 Java Selector

Linux epoll 的水平触发(LT)与边缘触发(ET)模式与 Java Selector 有何关系?

  • LT 与 ET 的区别
  • Java Selector 使用的模式
  • ET 模式在 Java 中的实现

epoll 支持水平触发(LT,Level-Triggered)和边缘触发(ET,Edge-Triggered)两种模式。LT 模式下,只要 fd 事件保持就绪就持续报告;ET 模式下,只在事件状态变化时报告一次。Java NIO 的 Selector 在 Linux 上使用 epoll 时默认采用 LT 模式,即 Selector 采用水平触发语义,只要通道可读/可写就持续返回就绪事件。Java 标准库没有直接暴露 EPOLLET(边缘触发)选项,因此 Java 开发者无法通过标准 Selector 使用 ET 模式。ET 模式需要手动处理(如配合非阻塞循环读完所有数据),通常由 Netty 等框架通过原生 epoll(如 netty 的 Epoll 模块)或监听层实现。LT 模式对 Java 更友好,因为编程简单、不易丢事件,但可能需要重复返回就绪事件。理解 LT 与 ET 的区别及 Java 默认 LT 是本题关键。

核心是 Java Selector 默认 LT,标准库不暴露 ET。ET 需要手动读完数据,通常由框架(Netty 原生 epoll)实现。理解默认 LT 与 ET 的差异是本题关键。

#
★★

38. NIO Selector 在 Linux 下底层 epoll 与 macOS 下 kqueue 的可移植性边界

NIO Selector 在 Linux 下基于 epoll、在 macOS 下基于 kqueue,两者之间的可移植性边界是什么?

  • epoll 与 kqueue 的差异
  • NIO Selector 跨平台的可移植性
  • 边界与注意事项

Java NIO Selector 在 Linux 上基于 epoll,在 macOS/BSD 上基于 kqueue,两者都提供事件多路复用,但存在可移植性边界。epoll 是 Linux 特有的,kqueue 是 BSD/macOS 的机制,两者在 API、事件语义、限制上有差异。对 Java 开发者而言,NIO 的 Selector 抽象了这些差异,提供了统一的 API(open/register/select),因此正常使用下是可移植的。但边界在于:某些平台特定行为并不完全一致,例如就绪事件报告的细节、唤醒延迟、对非寄存器 fd 的处理、某些 edge case(如 epoll 空轮询 bug 在 kqueue 上不存在)等。此外,kqueue 支持的文件监控(WatchService)与 epoll 的文件监控机制不同。测试时应在各平台运行以验证可移植性。总体上,Java 的 Selector 抽象了 epoll/kqueue 的差异,使业务代码可移植,但底层行为差异仍需在真实平台上验证。理解抽象层与平台差异的边界是本题关键。

可移植性边界是"统一 API 抽象 + 平台底层差异"。Selector 屏蔽了 epoll/kqueue 差异,但某些行为(如空轮询 bug、唤醒语义)仍因平台而异。理解抽象与差异的边界是本题关键。

#
★★

39. NIO Selector 的 native 方法注册顺序是否影响启动

NIO Selector 的 native 方法注册顺序是否会影响启动?为什么?

  • native 方法(System.loadLibrary)的注册时机
  • Selector 的初始化过程
  • 启动顺序与类加载

NIO Selector 的底层实现依赖 native 方法,这些方法通过 JNI 注册。在 JDK 中,native 库(如 libnio.so)通常在 Selector 相关类被加载时通过 System.loadLibrary 或静态初始化块加载。native 方法的注册顺序在正常启动流程下由类加载的顺序决定,一般不会导致问题,因为 JDK 封装了库的加载与初始化。但若在自定义代码中提前调用需要 native 支持的方法,而相关 native 库尚未加载,可能抛出 UnsatisfiedLinkError 或 NoClassDefFoundError。因此,启动时确保 native 库加载(通常由 JDK 自动完成)即可。开发者一般无需关心 native 方法注册顺序,因为 JDK 在类初始化时保证库加载。但若使用自定义类加载器或提前暴露 NIO 操作,需注意类初始化顺序。总体而言,native 方法注册顺序不影响正常启动,除非环境异常(如库缺失、类加载器隔离)。

native 方法注册顺序由类加载与库加载决定,JDK 自动保证。正常启动不受影响,异常环境(库缺失、类加载器隔离)才可能出问题。理解类加载与库加载的时机是本题关键。

#
★★

40. NIO Selector 的 select(long) 与 selectNow() 在低流量场景下的 CPU 唤醒率差异如何用 JFR 量化

NIO Selector 的 select(long) 与 selectNow() 在低流量场景下的 CPU 唤醒率差异如何用 JFR 量化?

  • select(timeout) 与 selectNow() 的差异
  • 低流量下的 CPU 唤醒率
  • JFR 进行性能分析

Selector.select(long timeout) 会阻塞等待事件,直到超时或事件就绪,适合需要定时唤醒的场景;selectNow() 是非阻塞的,立即返回,即使没有事件也会返回 0。在低流量场景下,若使用 selectNow() 循环轮询,会在没有事件时反复立即返回,导致高频空轮询,CPU 唤醒率极高,浪费 CPU;而 select(timeout) 会阻塞等待,唤醒率低,CPU 占用低。因此低流量场景应使用 select(timeout) 或 select() 阻塞,而不是 selectNow() 空转。用 JFR(JDK Flight Recorder)量化差异:可以记录 CPU 利用率、线程唤醒次数、JFR 的线程调度事件(如 jdk.ThreadPark、jdk.JavaMonitorEnter)以及 CPU 采样事件,对比 selectNow 循环与 select(timeout) 的 CPU 占用百分比与唤醒次数。通过 JFR 的 CPU 采样火焰图(jdk.ExecutionSample)可直观看到 selectNow 空转占用大量 CPU。JFR 还提供 jdk.ThreadSleep、jdk.ThreadPark 等事件帮助分析线程状态。

核心是 select(timeout) 阻塞降低唤醒率,selectNow() 空转抬高 CPU。JFR 的 CPU 采样与线程事件可量化对比。理解低流量下选择阻塞式 select 与用 JFR 分析是本题关键。

#
★★

41. NIO Selector 的空轮询 bug 与 Netty 的解决

NIO Selector 的空轮询 bug 是什么?Netty 是如何解决的?

  • 空轮询 bug 的成因
  • Netty 的解决方案
  • 空轮询的检测与规避

NIO Selector 的空轮询 bug 是指:在 Linux 上,当 epoll_wait 在无事件时错误地立即返回(或返回可读事件但实际无数据),导致 Selector.select() 循环在没有实际事件时就绪的情况下反复返回,形成空转死循环,CPU 占用飙升到 100%。根本原因是 Linux 内核 epoll 在特定负载下的缺陷(如连接数多、fd 密集时 epoll_wait 提前返回)。Netty 的解决方案是:在 Selector 循环中记录 select 返回的时间与次数,若在一定时间窗口内(如 100ms)空转次数超过阈值(如 512 次),则认为发生了空轮询 bug,此时新建一个 Selector,并把原有 Selector 上注册的所有 Channel 迁移到新 Selector(重新注册),从而打断空转死循环。这种"重建 Selector"的策略也被其他框架借鉴。Netty 还通过将 select 超时设置为不满的固定值来避免无限阻塞。理解空轮询 bug 的成因与 Netty 的重建策略是本题关键。

空轮询 bug 是 epoll 缺陷导致的无事件空转,Netty 用"空转次数检测 + 重建 Selector 迁移 Channel"解决。理解检测阈值与重建流程是本题关键。

#
★★

42. NIO.2 与 Path 的 relativize/resolve

NIO.2 中 Path 的 relativize 与 resolve 方法的作用是什么?如何使用?

  • resolve 与 relativize 的语义
  • Path 的路径操作
  • 使用场景

Path 的 resolve(Path other) 方法用于将当前路径与另一个路径合并,当 other 是绝对路径时直接返回 other,当 other 是相对路径时拼接在当前路径之后,返回相对简洁的路径。常用于把子路径拼接到基础路径上。relativize(Path other) 方法与 resolve 相反,它计算从当前路径到 other 的相对路径,即"如何从当前路径到达 other 路径"。两者是互逆操作:base.resolve(base.relativize(child)) 通常等于 child。例如 Paths.get("/a/b").resolve("c") 得到 /a/b/c,Paths.get("/a/b").relativize(Paths.get("/a/b/c")) 得到 c。NIO.2 中 Path 还提供 normalize()(去除冗余的 . 和 ..)、toAbsolutePath() 等。resolve 与 relativize 常用于路径拼接、相对路径计算、文件系统导航等场景。理解两者语义与互逆关系是本题关键。

resolve 是拼接,relativize 是计算相对路径,二者互逆。理解参数类型(绝对/相对)与互逆关系是使用 Path 的关键。

Path base = Paths.get("/a/b");
Path joined = base.resolve("c");          // /a/b/c
Path rel = base.relativize(Paths.get("/a/b/c")); // c
#
★★

43. NIO(同步非阻塞)模型与 Selector 多路复用

NIO(同步非阻塞)模型与 Selector 多路复用是如何结合的?其工作原理是什么?

  • NIO 的非阻塞特性
  • Selector 多路复用
  • 事件驱动的线程模型

NIO(New I/O)采用同步非阻塞模型:通道(Channel)配置为非阻塞后,读写操作立即返回,不阻塞线程,通过返回值判断是否完成。Selector 多路复用负责监控多个通道的事件:一次 select() 调用可以同时监听多个通道的 OP_ACCEPT、OP_READ、OP_WRITE、OP_CONNECT 等就绪事件。当有事件就绪时,select 返回,通过 selectedKeys() 获取就绪的通道集合,再逐个处理。这样单个线程通过 Selector 可以管理大量连接,避免了 BIO 的 1:1 线程模型。结合方式是:非阻塞通道注册到 Selector,事件循环线程循环 select() 处理就绪事件,对每个就绪通道执行对应的读写操作。这是 Netty 等框架的核心模型。NIO 的"同步"体现在读写仍需线程自己执行(事件就绪后由线程读取),"非阻塞"体现在不会因等待数据而阻塞线程。理解 NIO 与 Selector 的结合(事件循环)是掌握高性能网络编程的关键。

NIO 是同步非阻塞 + Selector 多路复用。核心是"单线程 + 事件循环"管理大量连接。理解同步(读写由线程执行)与非阻塞(不阻塞等待)及 Selector 分工是本题关键。

#
★★

44. Netty EpollEventLoopGroup 与 JDK Selector

Netty 的 EpollEventLoopGroup 与 JDK Selector 有何关系?它有什么优势?

  • EpollEventLoopGroup 与 NioEventLoopGroup 的区别
  • 基于原生 epoll 的实现
  • 相比 JDK Selector 的优势

Netty 的 EpollEventLoopGroup 是使用原生 epoll(通过 JNI 访问 Linux 的 epoll API)实现的 EventLoopGroup,而默认的 NioEventLoopGroup 基于 JDK NIO 的 Selector。两者都是 Netty 的事件轮询组,但 EpollEventLoopGroup 直接使用 Linux 原生 epoll,避免了 JDK NIO 的 Selector 的某些问题(如空轮询 bug),并支持更多原生特性:如 EPOLLET 边缘触发、EPOLL_RDHUP(对端关闭半开检测)、EPOLLEXCLUSIVE、对 socket 的更多控制。性能上,原生 epoll 减少了 JDK 抽象层的开销,在高并发场景下有优势。EpollEventLoopGroup 仅在 Linux 上可用,需要添加 netty-transport-native-epoll 依赖。选择时:Linux 生产环境可优先考虑 EpollEventLoopGroup 以获得更好的性能与原生特性,但要注意其 Linux 平台限制。理解 EpollEventLoopGroup 基于原生 epoll 替代 JDK Selector 是本题关键。

核心是 EpollEventLoopGroup 用原生 epoll 替代 JDK NIO Selector,支持 ET、RDHUP 等特性并规避空轮询 bug。它仅适用于 Linux。理解其与 JDK Selector 的关系及优势是本题关键。

#
★★

45. Netty 与 JDK NIO 的边界

Netty 与 JDK NIO 的边界在哪里?Netty 在 JDK NIO 之上做了哪些增强?

  • Netty 与 JDK NIO 的层次关系
  • Netty 的封装与增强
  • 何时选用 Netty

JDK NIO 是 Java 提供的底层 I/O 抽象(Channel、Buffer、Selector),提供非阻塞 I/O 与多路复用能力,但需要开发者自己处理事件循环、协议编解码、粘包拆包、线程模型等复杂问题。Netty 是在 JDK NIO(或原生 epoll)之上构建的网络框架,封装了 JDK NIO 的复杂细节,提供了:事件循环模型(EventLoopGroup)、Channel 抽象与生命周期管理、ByteBuf 内存管理(池化、引用计数)、编解码器(Codec)、粘包拆包(帧解码器)、优雅关闭、背压、超时与心跳、可扩展的 Pipeline 责任链等。边界在于:JDK NIO 是底层基础设施,Netty 是上层应用框架。当业务需要高性能网络编程且不希望自己实现复杂细节时,选择 Netty;当只需简单的 NIO 操作或需要最底层控制时,可直接使用 JDK NIO。Netty 不改变 JDK NIO 的本质,而是封装与增强。理解 Netty 与 JDK NIO 的层次与增强是本题关键。

边界是"底层基础设施 vs 上层框架"。Netty 封装 JDK NIO 并增强(ByteBuf、编解码、Pipeline、线程模型)。理解选择依据(复杂度 vs 控制力)是本题关键。

#
★★

46. ReadableByteChannel/WritableByteChannel 的接口语义

ReadableByteChannel 与 WritableByteChannel 接口的语义是什么?它们如何构成通道读写的基础?

  • ReadableByteChannel 与 WritableByteChannel 接口
  • read/write 与 ByteBuffer 的关系
  • 通道接口的层次

ReadableByteChannel 接口定义了 int read(ByteBuffer dst) 方法,表示从通道读取字节到 ByteBuffer,返回实际读取的字节数,返回 -1 表示 EOF。WritableByteChannel 接口定义了 int write(ByteBuffer src) 方法,表示将 ByteBuffer 中的字节写入通道,返回实际写入的字节数。这两个接口是通道读写的基础抽象,ByteChannel 接口同时继承两者(并继承 Closeable)。FileChannel、SocketChannel 等具体通道都实现了这些接口。read 操作会从通道读取数据填充 dst,读入的字节数取决于通道可用数据与缓冲区剩余空间;write 操作从 src 读取字节写入通道,对于非阻塞通道可能只写入部分字节。理解这两个接口的语义(read/write 与 ByteBuffer、返回-1 表示 EOF)是掌握通道读写的基础。

核心是 read/write 方法以 ByteBuffer 为媒介,返回实际字节数,-1 表示 EOF。ByteChannel 综合两者。理解接口语义与返回值是通道编程的基础。

#
★★

47. Selector 与 EventLoop 是 1:1 还是 N:1

Selector 与 EventLoop 的关系是 1:1 还是 N:1?Netty 中如何组织?

  • Selector 与 EventLoop 的对应关系
  • Netty 的 EventLoop 模型
  • 线程与 Selector 的绑定

在 Netty 中,一个 EventLoop 对应一个线程,且每个 EventLoop 通常绑定一个 Selector,即 1:1 关系。一个 EventLoop 线程只处理自己绑定的 Selector 的事件循环,所有的注册、读写、调度都在该线程内完成,保证对 Channel 的访问在该线程内串行化,避免并发冲突。多个 EventLoop 组成 EventLoopGroup,每个 EventLoop 管理一部分 Channel。因此整体上是"多个 EventLoop,每个 EventLoop 一个 Selector,1:1",而不是一个 EventLoop 共享多个 Selector 或 N:1。JDK 原生 Selector 本身没有 EventLoop 概念,但 Netty 将 Selector 封装进 EventLoop,实现线程与 Selector 的绑定。理解 1:1 关系(事件循环线程与 Selector 绑定)是理解 Netty 线程模型的关键。

核心是每个 EventLoop 一个线程、一个 Selector,1:1 绑定,保证线程内串行。多个 EventLoop 构成组。理解 1:1 与线程绑定的语义是本题关键。

#
★★

48. Selector 与 kqueue(BSD/macOS)的关系

Selector 在 macOS/BSD 上是如何基于 kqueue 实现的?两者的关系是什么?

  • kqueue 事件通知机制
  • macOS 上 Selector 的实现
  • kqueue 与 epoll 的关系

在 macOS/BSD 平台上,Java NIO 的 Selector 底层基于 kqueue 实现。kqueue 是 BSD/macOS 提供的高效事件通知机制,通过 kevent 注册感兴趣的事件(可读、可写、连接、文件变更等),并等待事件发生。Java 的 KQueueSelectorProvider 将 Selector 的注册(register)、轮询(select)、事件获取封装为 kqueue 的 kevent 调用。kqueue 与 Linux 的 epoll 功能类似(都是事件多路复用),但实现不同,kqueue 还支持文件系统事件(如 WatchService 依赖 kqueue)。理解 Selector 与 kqueue 的关系:Selector 是 Java 的抽象 API,kqueue 是 macOS 上的底层实现,两者通过 KQueueSelectorProvider 桥接。javac 与 JDK 通过 SelectorProvider 的 SPI 选择对应平台的实现。理解抽象 API 与 kqueue 底层实现的桥接是本题关键。

关系是"抽象 API 与底层实现"。macOS 上 Selector 用 kqueue 实现,通过 KQueueSelectorProvider 桥接。理解平台 Provider 选择机制是本题关键。

#
★★

49. Selector 与虚拟线程结合使用时,单连接单线程模型下 fd 数量上限是否会突破传统 1024 文件描述符限制

Selector 与虚拟线程结合使用时,单连接单线程模型下 fd 数量上限是否会突破传统 1024 文件描述符限制?

  • 文件描述符(fd)数量限制
  • Selector 与虚拟线程的 fd 使用
  • ulimit 与实际 fd 上限

传统 1024 文件描述符限制是操作系统对单个进程可打开 fd 数量的默认软限制(ulimit -n),并非 Java 或 Selector 固定限制。使用 Selector 与虚拟线程时,连接数若超过该限制,会因 fd 耗尽而失败,但这是操作系统层面的限制,与虚拟线程或 Selector 本身无关。虚拟线程允许大量并发连接(每个连接一个虚拟线程),但每个连接仍占用一个 socket fd,因此 fd 数量上限由系统 ulimit 决定。要突破 1024 限制,需通过 ulimit -n 或系统配置提高 fd 上限(如修改 /etc/security/limits.conf)。Selector 本身不占用额外 fd 于连接(一个 Selector 管理多个连接,但每个连接仍一个 fd)。因此,单连接单线程模型下 fd 数量上限不会因虚拟线程而自动突破,仍需提高系统 ulimit。理解 fd 限制(系统层面)与虚拟线程/Selector 的关系是本题关键。

核心是 fd 限制是操作系统 ulimit 决定的,与虚拟线程、Selector 无关。虚拟线程虽可承载大量连接,但每个连接仍占一个 fd。突破需提高 ulimit。理解这一点是本题关键。

#
★★

50. Selector 在 CompletableFuture 链中的阻塞问题

Selector 在 CompletableFuture 链中调用时有哪些阻塞问题?应如何处理?

  • CompletableFuture 中的阻塞操作
  • Selector.select 的阻塞特性
  • 异步链中避免阻塞

CompletableFuture 用于异步编排,其回调在线程池中执行。若在 CompletableFuture 的链式回调中调用 Selector.select()(阻塞方法),会阻塞执行该回调的线程池线程,可能耗尽线程池线程,导致其他任务无法执行。例如在 thenApply 中调用 select() 会阻塞默认 ForkJoinPool 或自定义线程池的线程。若链中的阻塞操作与 CompletableFuture 的异步线程池共用,可能造成线程饥饿。处理方式:不要在与 CompletableFuture 共用的线程池中执行阻塞的 Selector 操作;应使用专门的 I/O 线程(如独立的 EventLoop)执行 Selector 轮询,将结果通过 CompletableFuture.complete 传递给异步链;或使用 thenComposeAsync 指定独立线程池;对于阻塞操作应使用 supplyAsync 提交到专门的线程池,避免占用公共线程池。核心是"阻塞操作与异步线程池分离",避免阻塞公共线程造成线程饥饿。

核心是 CompletableFuture 回调中阻塞会占用并耗尽线程池线程。应将阻塞的 Selector 操作放到专门的 I/O 线程,通过 complete 传递结果。理解阻塞与异步线程池的隔离是本题关键。

#
★★

51. Selector 在 JDK 25 虚拟线程下的协作

Selector 在 JDK 25 虚拟线程下如何协作?有什么注意事项?

  • 虚拟线程中使用 Selector
  • 阻塞与 pinning
  • 与阻塞式 I/O 的对比

在 JDK 25 中,虚拟线程可以在内部使用 Selector 进行多路复用,但需要谨慎。虚拟线程的主要优势是用阻塞式 I/O 编写高并发代码,而 Selector 多路复用是"一个线程管理多个连接"的非阻塞模型,两者思路不同。若在虚拟线程中调用 Selector.select() 阻塞,可能触发 pinning(虚拟线程被钉住 carrier 线程),降低并发效率。官方建议:虚拟线程场景优先使用阻塞式 Socket I/O(一个虚拟线程一个连接),而不是 Selector 多路复用;Selector 多路复用更适合平台线程的 EventLoop 模型。若必须在虚拟线程中使用 Selector,应评估 pinning 影响。此外,虚拟线程与 Selector 的非阻塞 read/write 也可以配合,但要注意不要在虚拟线程中做长期的阻塞 select。理解虚拟线程与 Selector 的协作方式(阻塞式 I/O 优先)是本题关键。

核心是虚拟线程优先用阻塞式 I/O,Selector 多路复用并非其推荐场景,select 阻塞可能引发 pinning。理解官方建议与协作边界是本题关键。

#
★★

52. Selector 在 Windows 上的实现差异

Selector 在 Windows 上的实现与其他平台有何差异?

  • Windows 上 Selector 的底层实现
  • 与 epoll/kqueue 的差异
  • Windows 特有行为

在 Windows 上,Java NIO 的 Selector 基于 Winsock 的 select 模型实现(实际上是封装了 Windows 的 select 系统调用),而不是 IOCP。IOCP(I/O Completion Port)是 Windows 真正的异步 I/O 机制,用于 Java AIO(AsynchronousChannel)而非默认的 Selector。因此 Windows 上 Selector 的实现与前 Linux 的 epoll、macOS 的 kqueue 不同,它使用 select 模型,一次 select 最多支持 64 个 fd(Winsock select 的 FD_SETSIZE 限制),JDK 通过内部处理突破了这一限制(分段处理或使用扩展)。Windows 上 Selector 的唤醒、非阻塞语义与 Unix 平台有差异,例如对非阻塞 connect 的返回、OP_CONNECT 的触发时机等可能不同。因此跨平台使用 Selector 时需注意 Windows 的行为差异。理解 Windows 上 Selector 基于 select(而非 IOCP)是本题关键。

核心是 Windows 上默认 Selector 基于 select 模型,非 IOCP;AIO 才用 IOCP。存在 FD_SETSIZE 限制与行为差异。理解平台差异是本题关键。

#
★★

53. Selector 在高并发下的 epoll 性能

Selector 在高并发下基于 epoll 的性能如何?有哪些性能考量?

  • epoll 在高并发下的性能优势
  • Selector 的性能瓶颈
  • 优化手段

epoll 在高并发下具有优异性能,因为它采用事件驱动,只返回就绪的 fd,避免了 select 的线性扫描全部 fd。Java Selector 基于 epoll 时,能高效管理大量连接。但高并发下仍有性能瓶颈:单 Selector 线程成为瓶颈(事件处理、兴趣事件更新、系统调用都集中在一个线程);频繁修改 interestOps 调用 epoll_ctl 产生系统调用开销;就绪事件的处理耗时(如半包、业务逻辑)会阻塞事件循环;selectedKeys 的遍历与处理。优化手段:使用多事件循环(多 Selector 线程,如 Netty 的 EventLoopGroup);合并/延迟 interestOps 更新减少系统调用;使用边缘触发(Netty 原生 epoll)减少重复唤醒;避免在事件循环中执行耗时操作,将业务逻辑提交到工作线程池;使用零拷贝(transferTo)减少数据拷贝。理解 epoll 性能优势与 Selector 瓶颈及优化是本题关键。

核心是 epoll 事件驱动高效,但单 Selector 线程、interestOps 更新、事件处理耗时是瓶颈。优化通过多事件循环、减少系统调用、业务线程池分离。理解瓶颈与优化是本题关键。

#
★★

54. Selector 报告可读只表示读取可能推进,非阻塞 read 返回零或负一时分别应如何处理

Selector 报告可读只表示读取可能推进,此时非阻塞 read 返回零或负一分别应如何处理?

  • 就绪事件与读取结果的差异
  • read 返回 0 与 -1 的语义
  • 非阻塞读的处理

Selector 报告可读只表示"读取操作可能推进",即通道上可能有数据或者在连接语义上可读,但并不意味着一定能读到数据。此时调用非阻塞 read():若返回正数,表示读到了数据;若返回 0,表示当前没有可读数据(缓冲区可能为空,或读操作未推进),应继续等待后续就绪事件,不应视为错误;若返回 -1,表示对端已关闭(EOF),连接到达流结束,应关闭通道或处理连接结束。正确处理:read 返回 0 时,不要立即认为有数据,继续等待 select 的后续可读事件;read 返回 -1 时,应关闭连接、释放资源,并处理连接关闭逻辑。若可读事件持续但 read 返回 0,可能触发空转,需注意。理解就绪事件与 read 返回值的差异(0 表示无数据、-1 表示 EOF)是本题关键。

核心是就绪事件≠一定能读到数据。read 返回 0 表示无数据继续等待,-1 表示 EOF 关闭连接。理解这两个返回值与就绪事件的关系是本题关键。

#
★★

55. Selector 的 select/selectedKeys 流程

Selector 的 select 与 selectedKeys 流程是怎样的?如何使用?

  • select() 与 selectedKeys() 的配合
  • 就绪键集合与迭代处理
  • 清除已处理键

Selector 的典型使用流程是:调用 select() 阻塞等待直到至少一个通道就绪,select 返回就绪通道的数量;然后通过 selectedKeys() 获取就绪的 SelectionKey 集合;遍历该集合,对每个 key 判断其 interestOps(isReadable/isWritable/isAcceptable/isConnectable),并执行对应的操作;处理完一个 key 后,必须调用 iterator.remove() 将 key 从 selectedKeys 集合中移除,否则下次 select 时该 key 仍会残留,导致重复处理。select 返回后,selectedKeys 集合只包含本次就绪的 key,但若不移除,会累积。因此流程是"select -> selectedKeys -> 遍历处理 -> iterator.remove()移除已处理 key"。理解 select 与 selectedKeys 的配合及移除已处理 key 的流程是本题关键。

核心是 select 阻塞等待,selectedKeys 获取就绪键,遍历处理并必须 iterator.remove() 移除。理解移除语义避免重复处理是本题关键。

while (true) {
    selector.select();
    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    while (it.hasNext()) {
        SelectionKey key = it.next();
        it.remove();                    // 必须移除,防止重复处理
        if (key.isReadable()) { /* read */ }
    }
}
#
★★

56. Selector 的关闭与资源释放

Selector 的关闭与资源释放应如何处理?需要注意什么?

  • Selector.close() 的语义
  • 关闭与通道、fd 的关系
  • 资源释放的正确性

Selector 使用完应调用 close() 释放资源。close() 会关闭 Selector,使所有已注册的 SelectionKey 失效,并释放底层文件描述符(如 epoll fd)。关闭 Selector 前,应确保其上的 Channel 也已关闭或明确处理,因为关闭 Selector 会导致已注册通道的 key 失效,但通道本身可能仍打开。若关闭 Selector 时仍有阻塞的 select 线程,close() 会唤醒该线程。资源释放的正确做法:在 try-with-resources 或 finally 中关闭 Selector;优先关闭通道再关闭 Selector;注意避免关闭已关闭的 Selector 抛异常。应使用 try-with-resources(Selector 实现 Closeable)自动关闭。理解 Selector 关闭与通道、fd 资源释放的关系,避免 fd 泄漏,是本题关键。

核心是 close() 释放 fd 并使 key 失效,应配合通道关闭并正确管理资源。理解关闭顺序与 fd 释放是本题关键。

#
★★

57. Selector 的核心 API(open/register/select)

Selector 的核心 API open、register、select 分别是什么作用?如何使用?

  • Selector.open() 创建
  • Channel.register 注册
  • select() 轮询

Selector 的核心 API 包括:open() 创建 Selector 实例,内部会创建底层多路复用器(如 Linux 的 epoll);register() 是 Channel 的方法,将通道注册到 Selector 并指定感兴趣的 Operation(OP_ACCEPT、OP_READ、OP_WRITE、OP_CONNECT),返回 SelectionKey;select() 阻塞等待注册通道中至少一个就绪,返回就绪数量,另有 select(timeout) 与 selectNow()(非阻塞)。使用流程:Selector.open() 创建 -> 通道配置非阻塞 -> channel.register(selector, op) 注册 -> 循环 select() 处理就绪事件。SelectionKey 封装了通道、Selector 与兴趣操作,可通过 key 获取通道与就绪状态。理解这三个核心 API 的配合是掌握 Selector 编程的基础。

核心是 open 创建、register 注册、select 轮询的流程。理解三者配合与 SelectionKey 是本题关键。

#
★★

58. Selector 资源是否应在模块销毁时主动 close 避免 fd 泄漏

Selector 资源是否应在模块销毁时主动 close?如何避免 fd 泄漏?

  • Selector 的关闭时机
  • fd 泄漏的风险
  • 生命周期管理

是的,Selector 资源应在模块销毁时主动 close,以避免文件描述符(fd)泄漏。每个 Selector 在 Linux 上占用一个 epoll fd,若长期不关闭,会不断消耗 fd,最终耗尽系统 fd 上限导致连接失败。主动关闭的做法:在模块/组件的生命周期结束时(如停止事件循环、shutdown 回调中)调用 selector.close(),并确保所有注册的通道也关闭。应用 try-with-resources(Selector 实现 Closeable)或在 finally 中关闭。若 Selector 由框架管理(如 Netty 的 EventLoopGroup),框架会负责关闭。注意:Selector 关闭后其上的 key 失效,若还有线程阻塞在 select 需先唤醒。主动管理 Selector 生命周期、配合通道关闭,是避免 fd 泄漏的关键。

核心是 Selector 占用 fd,模块销毁需主动 close 避免泄漏。理解生命周期管理(try-with-resources、finally、框架管理)是本题关键。

#
★★

59. Selector.select() 与 selectNow()/select(timeout) 的差异

Selector.select()、selectNow() 与 select(timeout) 三者有何差异?各自适用什么场景?

  • select() 阻塞、selectNow() 非阻塞、select(timeout) 超时阻塞
  • 三者返回值与用途
  • 适用场景

select() 是阻塞等待,直到至少一个通道就绪才返回,返回就绪数量;selectNow() 是非阻塞的,立即返回当前就绪数量(可能为 0),不等待;select(timeout) 是带超时的阻塞,最多等待 timeout 毫秒,超时或就绪都返回。三者的差异在于阻塞行为:select() 无限阻塞(或直到唤醒),selectNow() 不阻塞,select(timeout) 有限阻塞。适用场景:select() 适合纯事件驱动、无定时任务的场景;selectNow() 适合需要在循环中处理其他任务(如定时器、非阻塞任务)时避免阻塞;select(timeout) 适合需要定期唤醒(如心跳、定时检查)的场景。理解三者的阻塞语义与适用场景是本题关键。

核心是阻塞行为差异:无限阻塞、非阻塞、超时阻塞。理解各自适用场景(事件驱动、定时任务、心跳)是本题关键。

#
★★

60. Selector.wakeup 的调用可以合并意味着什么,先 wakeup 后 select 的下一次阻塞行为如何定义

Selector.wakeup 的调用可以合并意味着什么?先 wakeup 后 select 的下一次阻塞行为如何定义?

  • wakeup 的合并语义
  • 先 wakeup 后 select 的行为
  • wakeup 的时序保证

Selector.wakeup() 用于唤醒当前阻塞在 select() 的线程,使其立即返回。其"调用可以合并"意味着:连续多次调用 wakeup() 的效果等价于一次 wakeup(),因为唤醒是通过设置一个标志(或写入管道)实现的,被唤醒后标志被消费,多余的 wakeup 不会产生副作用。关键的语义保证是:如果在一个线程中先调用 wakeup(),然后紧接着调用 select(),那么这次 select() 会立即返回(不会阻塞),因为唤醒标志还处于置位状态。也就是说,wakeup 的生效会"滞后"到下一次 select 调用,即使该 select 在其后调用。这一语义使得生产者可以在调用 select 之前先 wakeup,确保 select 不会长时间阻塞。理解 wakeup 的可合并性(一次唤醒抵消多次)与先 wakeup 后 select 立即返回的语义是本题关键。

核心是 wakeup 通过标志实现,可合并(多次等价一次),且先 wakeup 后 select 时下一次 select 立即返回。理解这一时序保证是本题关键。

#
★★

61. Selector.wakeup() 的实现原理

Selector.wakeup() 的实现原理是什么?它如何唤醒阻塞的 select 线程?

  • wakeup 的底层机制(self-pipe trick)
  • 唤醒标志与管道
  • 对 select 的影响

Selector.wakeup() 的实现原理基于 self-pipe trick(自管道技巧):Selector 内部维护一个管道(Pipe),wakeup() 时向管道写入一个字节,使管道变为可读,从而让阻塞在 select() 上的线程因管道可读事件而立即返回。select() 返回后,处理事件时会读取该字节以清空管道。在 Linux 的 epoll 实现中,也通过类似机制(写入一个唤醒 fd)触发 select 返回。此外,wakeup 还会设置一个唤醒标志(wakeupCalled),保证"先 wakeup 后 select 时下一次 select 立即返回"的语义。wakeup 是线程安全的,可被任意线程调用,用于在另一个线程中唤醒阻塞的 select 线程。理解 wakeup 基于 self-pipe trick 与唤醒标志的机制是本题关键。

核心是 wakeup 通过向内部管道写入字节(self-pipe trick)触发 select 返回,并配合唤醒标志保证时序。理解这一机制是本题关键。

#
★★

62. SocketChannel 的连接、OP_CONNECT 与 finishConnect

SocketChannel 的非阻塞连接、OP_CONNECT 与 finishConnect 是如何工作的?

  • 非阻塞 connect
  • OP_CONNECT 与 finishConnect
  • 连接状态处理

对于非阻塞 SocketChannel,调用 connect() 发起连接是非阻塞的,立即返回 true(连接完成)或 false(连接进行中)。当连接进行中时,需要注册 OP_CONNECT 兴趣事件到 Selector,当连接完成时 select 会报告 OP_CONNECT 就绪,此时调用 finishConnect() 完成连接并确认连接结果。若连接失败,finishConnect() 会抛出 ConnectException。正确流程:注册 OP_CONNECT -> select 报告 OP_CONNECT 就绪 -> 调用 finishConnect() 确认连接成功 -> 清除 OP_CONNECT(通常改为 OP_READ)开始读写。注意:OP_CONNECT 就绪后必须调用 finishConnect(),否则连接未正式完成;且连接完成后应取消 OP_CONNECT 兴趣,否则可能反复触发。理解非阻塞 connect 与 OP_CONNECT/finishConnect 的配合是本题关键。

核心是非阻塞 connect 后注册 OP_CONNECT,就绪后需 finishConnect() 确认连接结果。理解连接状态机与 OP_CONNECT 处理是本题关键。

#
★★

63. SocketChannel 部分写入后为何只应在仍有待发数据时注册 OP_WRITE,否则会造成怎样的空转

SocketChannel 部分写入后为何只应在仍有待发数据时注册 OP_WRITE?否则会怎样空转?

  • 非阻塞写的部分写入
  • OP_WRITE 的就绪高频触发
  • 避免空转

非阻塞 SocketChannel 的 write() 可能只写入部分数据(返回写入字节数小于请求),此时仍有待发数据。若在缓冲区未满时持续注册 OP_WRITE,因为 socket 可写状态通常会一直保持(缓冲区未满),select 会持续报告 OP_WRITE 就绪,导致事件循环高频空转(busy loop),消耗 CPU。因此正确做法是:只有存在待发数据(缓冲未写完)时才注册 OP_WRITE 兴趣;当数据全部写完(缓冲清空)后,应取消 OP_WRITE 兴趣(interestOps 清除 OP_WRITE),避免持续的写就绪事件。否则,即使无需发送数据,OP_WRITE 也会因 socket 可写而频繁触发,造成大量无意义的 select 返回与空转,浪费 CPU。理解 OP_WRITE 的"始终就绪"特性与按需注册是本题关键。

核心是 socket 可写状态通常持续,OP_WRITE 会频繁就绪,若一直注册则空转。只在有待发数据时注册,写完取消。理解这一点避免 busy loop 是本题关键。

#
★★

64. SocketChannel/ServerSocketChannel 的非阻塞配置与 Selector 协作模式

SocketChannel/ServerSocketChannel 的非阻塞配置与 Selector 是如何协作的?

  • configureBlocking(false) 配置
  • 注册与事件驱动
  • 服务端/客户端的协作模式

非阻塞 I/O 的核心是配置通道为非阻塞模式(configureBlocking(false)),即通道的读写操作立即返回,不阻塞线程。服务端模式:ServerSocketChannel 配置非阻塞并注册 OP_ACCEPT 到 Selector,select 报告 OP_ACCEPT 就绪时调用 accept() 接受连接,得到的新 SocketChannel 也配置非阻塞并注册 OP_READ/OP_WRITE。客户端模式:SocketChannel 配置非阻塞,注册 OP_CONNECT 发起连接,就绪后 finishConnect,再注册读写。协作模式是:一个 Selector 监控多个非阻塞通道,select 阻塞等待就绪事件,就绪后逐个处理。非阻塞通道 + Selector 实现"单线程管理多连接"的高并发模型。理解非阻塞配置与 Selector 的协作(服务端 accept、客户端 connect、读写事件)是本题关键。

核心是 configureBlocking(false) 使通道非阻塞,配合 Selector 的 OP_ACCEPT/OP_CONNECT/OP_READ/OP_WRITE 事件驱动。理解服务端/客户端协作模式是本题关键。

#
★★

65. TLS 使用 SSLEngine 叠加 Selector 时,握手状态、网络缓冲和应用缓冲如何驱动兴趣事件

TLS 使用 SSLEngine 叠加 Selector 时,握手状态、网络缓冲和应用缓冲如何驱动兴趣事件?

  • SSLEngine 的握手状态机
  • 网络缓冲与应用缓冲(NEED_TASK/NEED_UNWRAP/NEED_WRAP)
  • 驱动 OP_READ/OP_WRITE 的规则

TLS 使用 SSLEngine 时,需要手动管理握手与加密数据。SSLEngine 返回的握手状态包括 NEED_UNWRAP(需要从网络读数据解密)、NEED_WRAP(敌方数据加密后写入网络)、NEED_TASK(需要执行阻塞任务,如密钥生成)、FINISHED(握手完成)。这些状态驱动 Selector 的兴趣事件:当 SSLEngine 需要网络数据(NEED_UNWRAP)时,注册 OP_READ;当 SSLEngine 有加密数据待发送(NEED_WRAP 且输出缓冲非空)时,注册 OP_WRITE。网络缓冲(net buffer,用于存放加密的 TLS 记录)与应用缓冲(app buffer,用于存放明文数据)的大小会影响读写:若 net 输出缓冲满,需注册 OP_WRITE 等待发送;若 net 输入缓冲满,需读走数据。握手期间,需要循环处理 NEED_TASK(执行任务)与响应的 wrap/unwrap,根据 SSLEngine 状态动态调整注册的 OP_READ/OP_WRITE。理解 SSLEngine 状态机驱动兴趣事件是本题关键。

核心是 SSLEngine 的 NEED_* 状态决定注册何种兴趣事件,网络缓冲与应用缓冲驱动读写。理解握手状态机与事件驱动的映射是本题关键。

#
★★

66. Windows IOCP 与 AIO 的实现差异

Windows IOCP 与 Java AIO 的实现有何差异?IOCP 在 AIO 中扮演什么角色?

  • IOCP(I/O Completion Port)机制
  • Windows 上 AIO 的实现
  • 与 Unix 端的差异

IOCP(I/O Completion Port)是 Windows 提供的高效异步 I/O 机制,内核完成 I/O 后通过完成端口通知应用。Java AIO(NIO.2)在 Windows 上基于 IOCP 实现,AsynchronousChannel 的读写操作提交给内核后,由 IOCP 在完成时回调,实现真正的异步 I/O。这与 Unix/Linux 上 AIO 基于 epoll 模拟不同:Windows 上是真正的内核异步(读写请求提交后由内核异步完成并通知),Linux 上是用 epoll 模拟异步(事件就绪后由线程执行读写)。IOCP 的优势是内核级异步,读写不阻塞线程,由完成端口高效分发完成事件。差异主要体现在:Windows 的 AIO 是"真异步"(内核完成),Linux 的 AIO 是"事件循环模拟异步";因此 Windows 上 AIO 性能与模型更贴近原生异步。理解 IOCP 与 AIO 的关系(Windows 基于 IOCP 实现真异步)是本题关键。

核心是 Windows 上 AIO 基于 IOCP 实现真内核异步,Linux 上基于 epoll 模拟异步。理解平台差异与 IOCP 机制是本题关键。

#
★★

67. java.util.concurrent.Future 与 AIO 的差异

java.util.concurrent.Future 与 AIO 的异步结果有何差异?

  • Concurrent Future 的语义
  • AIO 的 Future 与回调
  • 阻塞与非阻塞

java.util.concurrent.Future 是并发任务的结果抽象,通过 Future.get() 阻塞获取结果,或 isDone() 检查完成状态。AIO 的异步操作在传入 null CompletionHandler 时也返回一个 Future,用于获取 I/O 结果(如读写字节数)。两者的差异在于:并发 Future 通常由任务(Runnable/Callable)在 Executor 中执行完成,AIO 的 Future 则由内核异步 I/O 完成时设置结果;获取结果时,并发 Future 的 get() 阻塞等待任务完成,AIO Future 的 get() 阻塞等待内核 I/O 完成。AIO 还提供 CompletionHandler 回调的方式(非阻塞),而并发 Future 没有回调(需轮询 isDone 或阻塞 get)。可以说 AIO 的 Future 是 java.util.concurrent.Future 的特例,但 AIO 额外支持回调式异步。理解两者差异(阻塞获取 vs 回调、任务 vs 内核 I/O)是本题关键。

核心是并发 Future 由任务完成、AIO Future 由内核 I/O 完成,且 AIO 额外支持回调。理解阻塞与回调的差异是本题关键。

#
★★

68. 从其他线程修改 interestOps 或注册 Channel 时,如何配合任务队列与 wakeup 避免更新延迟

从其他线程修改 interestOps 或注册 Channel 时,如何配合任务队列与 wakeup 避免更新延迟?

  • interestOps 修改的线程安全
  • 任务队列与 wakeup
  • 避免延迟的处理

Selector 的事件循环通常运行在单独线程,从其他线程修改 interestOps 或注册 Channel 时,不能直接操作 Selector 的选择键(非线程安全),否则可能造成竞争或数据不一致。正确做法是:将 interestOps 修改或 Channel 注册封装为任务,提交到事件循环线程的任务队列中,由事件循环线程在下一个循环迭代中执行,从而保证串行化。同时,为让事件循环线程立即醒来处理任务,需要调用 Selector.wakeup() 唤醒阻塞在 select() 的线程。若只提交任务而不 wakeup,事件循环线程可能长期阻塞在 select() 上,任务得不到及时执行,造成更新延迟。因此"任务队列 + wakeup"配合使用:提交任务到队列,并 wakeup 唤醒 select 线程,使其及时处理任务。理解这一模式(提交任务 + wakeup 唤醒)是避免跨线程更新延迟的关键。

核心是跨线程操作需通过任务队列串行化到事件循环线程,并用 wakeup 唤醒 select 避免延迟。理解任务提交与唤醒的配合是本题关键。

#
★★

69. 使用 JDK Flight Recorder 分析 NIO Selector 空轮询 bug(CPU 100%)

如何使用 JDK Flight Recorder(JFR)分析 NIO Selector 空轮询 bug(CPU 100%)?

  • JFR 的 CPU 采样与事件
  • 空轮询的定位
  • 分析方法

使用 JFR 分析空轮询 bug 的步骤:开启 JFR 录制(如 -XX:StartFlightRecording 或 jcmd),录制期间在 CPU 100% 时采集数据。JFR 提供 jdk.ExecutionSample(CPU 采样)事件,可以生成火焰图,定位到 CPU 占用最高的方法(如 sun.nio.ch.EPollSelectorImpl 的 select 或相关循环)。通过 CPU 采样火焰图,可看到空轮询循环(Selector.select 循环反复执行)占用了绝大部分 CPU。还可结合 jdk.ThreadPark、jdk.JavaMonitorEnter 等事件分析线程状态。此外,JFR 的 jdk.ThreadSleep 可观察线程是否异常空转。分析要点:在 Flood 测试中录制 JFR,观察 select 方法调用频率与 CPU 占用,判断是否发生空轮询(select 无事件却频繁返回)。通过 JFR 的 CPU 采样与线程事件定位空轮询热点,是诊断 CPU 100% 的常用方法。

核心是 JFR 的 CPU 采样(jdk.ExecutionSample)火焰图定位空轮询热点(Selector.select 循环)。理解 JFR 录制与采样分析是本题关键。

#
★★

70. 使用 Selector 注册 OP_CONNECT 与 OP_WRITE 时,连接成功前的多次可写事件应如何避免 busy-loop

使用 Selector 注册 OP_CONNECT 与 OP_WRITE 时,连接成功前的多次可写事件应如何避免 busy-loop?

  • OP_CONNECT 与 OP_WRITE 的重叠
  • 连接成功前的可写事件
  • 避免 busy-loop

在非阻塞连接过程中,若同时注册 OP_CONNECT 和 OP_WRITE,连接成功前 socket 可能已处于可写状态,导致 OP_WRITE 频繁就绪,造成 busy-loop(空转)。正确做法是:连接过程中只注册 OP_CONNECT,等待连接完成(OP_CONNECT 就绪并调用 finishConnect 确认)后,再注册 OP_WRITE(或 OP_READ)。不要在连接成功前注册 OP_WRITE,因为连接未完成时可写状态并不代表真正可发送数据,且会持续触发。连接完成后,按需(有待发数据)注册 OP_WRITE,写完取消。若已注册 OP_WRITE 且连接进行中,应在 OP_CONNECT 就绪后先处理连接,再调整 interestOps。理解连接成功前避免注册 OP_WRITE 以规避 busy-loop 是本题关键。

核心是连接成功前只注册 OP_CONNECT,避免 OP_WRITE 的持续可写触发造成空转。理解连接状态与兴趣事件的关系是本题关键。

#
★★

71. 发生疑似空轮询时,重建 Selector 并迁移 Channel 需要保持哪些注册状态和并发顺序

发生疑似空轮询时,重建 Selector 并迁移 Channel 需要保持哪些注册状态和并发顺序?

  • 重建 Selector 的流程
  • 迁移 Channel 需保持的状态
  • 并发顺序与线程安全

重建 Selector 解决空轮询 bug 时,需要保持并迁移以下注册状态:每个 Channel 的 SelectionKey 的 interestOps(兴趣操作集合)、attachment(附加对象)、以及通道的当前状态(是否已注册、已连接的连接状态)。迁移流程:新建 Selector,遍历原 Selector 上的所有 key,对每个 key 获取其 Channel,将 Channel 重新注册(register)到新 Selector,并恢复其 interestOps 与 attachment;然后关闭旧 Selector。并发顺序:重建过程必须在事件循环线程内完成,避免其他线程同时修改;重建时需暂停事件处理(或确保单线程),迁移完成后恢复。若存在其他线程调用 wakeup 或修改 interestOps,需重新协调(如通过任务队列)。核心是保持 interestOps 与 attachment 的完整迁移,并保证单线程内完成以避免并发竞争。Netty 的重建策略即如此。理解需保持的状态与并发顺序是本题关键。

核心是迁移时恢复每个 key 的 interestOps 与 attachment,并在单线程内完成重建,避免并发竞争。理解重建流程与状态保持是本题关键。

#
★★

72. 同一 Channel 为什么不能注册到已关闭 Selector,关闭顺序如何避免文件描述符与任务泄漏

同一 Channel 为什么不能注册到已关闭的 Selector?关闭顺序如何避免文件描述符与任务泄漏?

  • 已关闭 Selector 的注册限制
  • 关闭顺序与资源释放
  • 避免 fd 与任务泄漏

Selector 关闭后,其底层的多路复用器(如 epoll fd)已释放,此时再向该 Selector 注册 Channel 会抛出 ClosedSelectorException,因为 Selector 已无法监控新事件。因此同一 Channel 不能注册到已关闭的 Selector。关闭顺序对资源释放至关重要:应先关闭/取消所有已注册的 Channel 或取消其 key、停止任务提交,再关闭 Selector,从而避免 fd 泄漏与任务泄漏。正确顺序:先停止事件循环(避免继续提交任务)、取消所有注册的 key 并关闭 Channel(释放各自的 fd)、最后关闭 Selector(释放多路复用器 fd)。若先关闭 Selector 再处理 Channel,会导致 key 失效、Channel 无法正常注销,且可能遗留任务队列中的任务。此外,若未关闭 Channel 就关闭 Selector,Channel 的 fd 仍打开,造成 fd 泄漏。理解注册限制与关闭顺序(先通道后 Selector)是避免泄漏的关键。

核心是已关闭 Selector 会抛 ClosedSelectorException,关闭顺序应为先停止循环、关闭通道、再关 Selector。理解顺序与泄漏的关系是本题关键。

#
★★

73. 每连接独占大 ByteBuffer 会造成什么直接内存压力,缓冲池与动态扩容如何避免数据串扰

每连接独占大 ByteBuffer 会造成什么直接内存压力?缓冲池与动态扩容如何避免数据串扰?

  • 每连接独占大缓冲的直接内存压力
  • 缓冲池与动态扩容
  • 数据串扰的避免

若每个连接独占一个大的直接 ByteBuffer(如 64KB),在高并发下(如十万连接)会占用巨大的直接内存(十万 × 64KB 约 6.4GB),且不受 -Xmx 堆限制,易造成直接内存溢出(OutOfMemoryError)。为缓解,应采用缓冲池(Buffer Pool)复用缓冲,避免每连接独立分配;或采用动态扩容(按需增长,如初始小缓冲,需要时扩展)避免固定大缓冲浪费。但复用与动态扩容带来数据串扰风险:多个连接复用同一缓冲时,若未及时清理,前一连接的数据可能残留污染后一连接;动态扩容时若缓冲被多个连接共享,读写指针或数据可能错乱。避免数据串扰的措施:每次使用后复位(clear/rewind)并明确读写边界;使用后及时释放回池;确保缓冲一用一收,不长期共享;通过引用计数或所有权转移保证同一时刻只有一个使用方。理解每连接独占大缓冲的压力与缓冲池/动态扩容的串扰控制是本题关键。

核心是每连接大缓冲造成直接内存压力,缓冲池/动态扩容缓解但需防数据串扰(复位、所有权、引用计数)。理解压力与串扰控制是本题关键。

#
★★

74. 虚拟线程下多个业务线程共享同一个 Selector 是否仍需 synchronized 保护 selectedKeys 集合

虚拟线程下多个业务线程共享同一个 Selector 时,是否仍需 synchronized 保护 selectedKeys 集合?

  • selectedKeys 集合的线程安全
  • Selector 的线程模型
  • 虚拟线程下的并发

是的,即便在虚拟线程下,多个业务线程共享同一个 Selector 时,仍需要 synchronized 保护 selectedKeys 集合(以及 select 的调用)。Select 的 selectedKeys 集合不是线程安全的,若多个线程同时 select 并遍历 selectedKeys,会产生竞态条件,导致数据错乱或异常。虚拟线程改变了线程的调度方式(轻量级),但不改变集合的线程安全性。标准做法是:Selector 的 select 与 selectedKeys 处理应由单一事件循环线程负责,避免多个线程同时操作;若确实多线程共享,需用 synchronized 保护对 selectedKeys 的访问与 select 的调用。此外,要注意 interestOps 的修改也应通过任务队列串行化。理解虚拟线程不改变 selectedKeys 的线程安全要求,仍需同步或单线程化,是本题关键。

核心是 selectedKeys 集合非线程安全,虚拟线程不改变这一点,多线程共享仍需 synchronized 或单事件循环线程。理解并发安全要求是本题关键。

#
★★

75. 虚拟线程阻塞式套接字与单线程 Selector 模型各适合什么负载,不能仅按连接数判断的原因是什么

虚拟线程阻塞式套接字与单线程 Selector 模型各适合什么负载?为什么不能仅按连接数判断?

  • 虚拟线程阻塞式 I/O 的适用负载
  • 单线程 Selector 的适用负载
  • 负载模型(连接数 vs 吞吐/活跃度)

虚拟线程阻塞式套接字适合"连接数多但每个连接不太活跃、或被阻塞式代码简单处理"的负载,因为虚拟线程让每个连接一个线程的成本变得廉价,适合大量长连接、简单请求。单线程 Selector 模型适合"单个事件循环处理高吞吐、低延迟、需要精细控制"的负载,如网关、代理、消息中间件,因为它在单线程内高效轮询,避免虚拟线程的上下文切换与调度开销。不能仅按连接数判断的原因是:负载的关键指标除了连接数,还包括每个连接的数据活跃度(QPS/吞吐)、处理逻辑复杂度、延迟要求、是否 I/O 密集。连接数多但活跃度低(如空闲长连接)适合虚拟线程;连接数少但每条连接吞吐极高(如文件传输、高频推送)可能更适合有多个事件循环的 Selector 模型。因此需综合连接数、活跃度、吞吐、延迟等判断。理解负载模型(连接数、活跃度、吞吐)而非仅连接数是本题关键。

核心是负载判断需综合连接数、活跃度、吞吐、延迟,而非仅连接数。虚拟线程适合大量低活跃连接,Selector 适合高吞吐低延迟。理解负载模型是本题关键。

#
★★

76. ByteBuffer 的 slice() 与数据共享

ByteBuffer 的 slice() 方法的作用是什么?它与数据共享有何关系?

  • slice() 创建子视图
  • 与底层共享数据
  • 独立 position/limit

ByteBuffer.slice() 在原始 ByteBuffer 的基础上创建一个子视图(slice),该视图的底层数据与原始 buffer 共享,即对 slice 的修改会反映到原始 buffer,反之亦然。slice() 从原始 buffer 的当前位置开始,长度等于剩余可读字节数(remaining),并将 slice 的 position 置 0、limit 置为剩余长度。slice 具有独立的 position、limit、mark,但与原始 buffer 共享底层数据。slice() 常用于:将 buffer 的一部分作为独立 buffer 处理,而不复制数据(零拷贝),例如在协议解析中把一段数据作为子视图传递,或避免重复分配。注意 slice 的生命周期与原始 buffer 的底层数据绑定,若原始 buffer 被回收或数据被覆盖,slice 会受影响。理解 slice() 的共享底层与独立位置语义是本题关键。

核心是 slice() 创建共享底层数据的子视图,提供独立位置但零拷贝。理解其共享语义与适用场景是本题关键。

ByteBuffer buf = ByteBuffer.wrap("hello".getBytes());
buf.position(2);
ByteBuffer slice = buf.slice();    // 共享数据,从 'l' 开始,长度 3
#
★★

77. JDK 25 NIO 2 的内部清理

JDK 25 中 NIO 2 的内部清理(cleanup)有哪些机制?主要涉及哪些资源?

  • NIO 2 的资源清理
  • Cleaner、通道关闭、WatchService
  • 资源回收机制

JDK 25 中 NIO 2 的内部清理机制涉及多个层面:直接缓冲(DirectByteBuffer)通过 Cleaner(基于 PhantomReference)在对象不可达后回收堆外内存;通道(Channel)通过 close() 释放对应文件描述符,并配合 Cleaner 在通道被 GC 前兜底清理(防止 fd 泄漏);WatchService 通过关闭机制释放底层文件监控资源;Selector 通过 close() 释放多路复用器 fd。JDK 通过这些机制保证 I/O 资源在对象不可达或被显式关闭时被回收。此外,JDK 25 对直接内存回收、Cleaner 的触发时机有优化。理解 NIO 2 的清理机制(Cleaner 回收直接内存、通道/Selector/WatchService 的 close 释放 fd)是本题关键。

核心是 NIO 2 通过 Cleaner 回收直接内存,通过 close 释放 fd(通道、Selector、WatchService)。理解这些清理机制与资源回收是本题关键。

#
★★

78. JDK 25 中 Selector.select 方法在 G1 暂停期间是否被阻塞,导致长尾延迟超出预期

JDK 25 中 Selector.select 方法在 G1 GC 暂停期间是否会被阻塞,导致长尾延迟超出预期?

  • G1 停顿与 JVM 暂停
  • Selector.select 的阻塞特性
  • 长尾延迟的原因

Selector.select() 是阻塞调用,在 G1 GC 发生暂停(STW,Stop-The-World)期间,所有 Java 线程都会暂停,包括阻塞在 select() 的线程,其 select 会因 JVM 暂停而延迟返回。G1 的 STW 暂停(如 young GC、mixed GC)会导致事件循环线程无法及时处理就绪事件,从而产生长尾延迟。虽然 select 本身是 native 阻塞,但 JVM 暂停会冻结所有线程,导致 select 返回延迟。因此,G1 暂停是 NIO 高并发场景长尾延迟的一个来源。缓解措施:调优 G1(减少停顿,如增大堆、调整目标停顿时间、使用 ZGC 等低延迟 GC)、减少大对象分配、避免频繁 GC。此外,selector 的空闲等待可通过 wakeup 不受影响。理解 G1 暂停会阻塞 select 并造成长尾延迟是本题关键。

核心是 G1 STW 暂停冻结所有线程,包括阻塞的 select,导致长尾延迟。缓解靠调优 GC。理解 GC 暂停对 select 的影响是本题关键。

#
★★

79. Selector 与 io_uring 的未来(JDK 25+ 提案)

Selector 与 io_uring 的未来是什么?JDK 25+ 是否有相关提案?

  • io_uring 作为未来 I/O 方向
  • JDK 对 io_uring 的提案
  • 高性能 I/O 演进

io_uring 是 Linux 提供的高效异步 I/O 接口,通过共享内存环形队列减少系统调用,支持真异步,性能优于传统 epoll。JDK 社区有关于引入 io_uring 作为 NIO/Selector 底层实现的提案与研究(如 JEP 相关讨论、panama 及内外部库),但截至 JDK 25 尚未正式将 io_uring 作为默认 NIO 底层,仍使用 epoll。未来方向是:JDK 可能引入对 io_uring 的支持,使 NIO/Selector 在 Linux 上获得更高性能与真异步能力。Netty 等框架已通过 incubator 项目(netty-incubator-transport-io_uring)使用 io_uring。io_uring 的引入需要考虑内核版本要求、可移植性、与现有 Selector 语义的兼容。理解 io_uring 作为未来高性能 I/O 方向与 JDK 提案现状是本题关键。

核心是 io_uring 是高性能 I/O 未来方向,JDK 有提案但尚未默认落地,仍用 epoll。理解现状与趋势是本题关键。

#
★★

80. ServerSocketChannel 的 accept() 与 OP_ACCEPT

ServerSocketChannel 的 accept() 与 OP_ACCEPT 事件是如何工作的?

  • ServerSocketChannel.accept()
  • OP_ACCEPT 就绪事件
  • 服务端接受连接

ServerSocketChannel 用于服务端监听连接。在阻塞模式下,accept() 会阻塞等待客户端连接;在非阻塞模式下,accept() 立即返回,若没有连接则返回 null。OP_ACCEPT 是注册到 Selector 的兴趣事件,表示"有新连接可接受"。当 Selector 报告 OP_ACCEPT 就绪时,说明有客户端连接到来,此时调用 accept() 会成功返回新的 SocketChannel(须非阻塞),然后该连接通道可注册 OP_READ/OP_WRITE 进行读写。服务端模式:ServerSocketChannel 配置非阻塞并注册 OP_ACCEPT,select 循环中判断 isAcceptable(),就绪时 accept() 获取连接。注意:OP_ACCEPT 就绪后 accept() 应快速完成,避免阻塞事件循环;accept 返回的 SocketChannel 需单独配置非阻塞并注册到 Selector。理解 accept() 与 OP_ACCEPT 的配合(就绪后 accept)是本题关键。

核心是 OP_ACCEPT 就绪表示有新连接可接受,此时 accept() 返回连接并注册读写。理解服务端 accept 流程是本题关键。

#

81. NIO Channel 接口层次与阻塞/非阻塞读写模型

NIO Channel 接口的层次结构是什么?阻塞/非阻塞读写模型如何?

  • Channel 接口层次
  • 阻塞与非阻塞读写
  • 各类 Channel 的定位

NIO Channel 是 I/O 的抽象,接口层次:最顶层 Channel 接口(继承 Closeable),下有 ReadableByteChannel(可读)、WritableByteChannel(可写)、ByteChannel(合并读写)、ScatteringByteChannel(分散读)、GatheringByteChannel(聚集写)、InterruptibleChannel(可中断)。具体实现包括 FileChannel(文件)、SocketChannel(TCP 客户端)、ServerSocketChannel(TCP 服务端)、DatagramChannel(UDP)。阻塞/非阻塞读写模型:阻塞模式(默认)下 read/write 阻塞直到完成;非阻塞模式(configureBlocking(false))下 read/write 立即返回,配合 Selector 进行多路复用。网络通道(SocketChannel 等)支持阻塞与非阻塞切换,并以 Selector 协作实现高并发;FileChannel 不支持非阻塞(不参与 Selector)。理解 Channel 接口层次与阻塞/非阻塞模型的差异是本题关键。

核心是 Channel 接口层次(读写/分散聚集)与阻塞/非阻塞两模型。网络通道支持非阻塞+Selector,文件通道不支持。理解层次与模型是本题关键。

#

82. JDK 25 中 KQueueSelectorProvider 在 macOS 上的兼容性与 NIO 可移植性测试

JDK 25 中 KQueueSelectorProvider 在 macOS 上的兼容性如何?NIO 可移植性测试如何做?

  • KQueueSelectorProvider 的兼容性
  • NIO 跨平台可移植性
  • 测试方法

KQueueSelectorProvider 是 JDK 在 macOS/BSD 上提供的 Selector 实现,基于 kqueue。在 JDK 25 中,KQueueSelectorProvider 在 macOS 上保持兼容,正常提供 Selector 的标准语义(open/register/select)。其兼容性关注点:kqueue 与 epoll 的语义差异(如就绪事件报告、唤醒行为)、macOS 版本对 kqueue 的支持。NIO 可移植性测试应:在多个平台(Linux/macOS/Windows)运行同一套测试,验证 Selector 的标准行为一致;编写与平台无关的断言(基于 Selector 的标准化语义);针对平台差异(如 kqueue 的某些行为)补充专项测试;通过抽象封装隔离平台差异,在 CI 中多平台运行。理解 KQueueSelectorProvider 的兼容性与跨平台测试方法是本题关键。

核心是 KQueueSelectorProvider 在 macOS 上兼容并为 Selector 提供标准语义,可移植性测试需多平台 CI 与平台无关断言。理解兼容性与测试方法是本题关键。

#

83. Selector.open() 的 epoll fd 创建时机与文件描述符资源治理

Selector.open() 在 Linux 上创建 epoll fd 的时机是什么?如何治理文件描述符资源?

  • open() 创建 epoll fd 的时机
  • fd 资源治理
  • 避免 fd 泄漏

在 Linux 上,Selector.open() 会创建内部多路复用器,通过 epoll_create 创建 epoll fd(也可能初始化管道用于 wakeup)。这个 fd 在 Selector 创建时即被分配,直到 close() 才释放。因此每个 Selector 占用至少一个(甚至两个)fd。fd 资源治理:合理控制 Selector 数量(避免频繁创建/销毁);及时 close() 释放 fd;监控 fd 数量(/proc/self/fd 或 lsof),设置 fd 上限告警;使用 try-with-resources 自动关闭。避免 fd 泄漏:确保 Selector 用完即关、异常路径也关闭;避免长期持有不用的 Selector。理解 open() 创建 epoll fd 的时机与 fd 治理(及时关闭、监控、限制数量)是本题关键。

核心是 open() 即创建 epoll fd,需及时 close 并监控 fd 数量避免泄漏。理解创建时机与治理手段是本题关键。

#

84. ByteBuffer 与 GraalVM Native Image 的边界

ByteBuffer 与 GraalVM Native Image 的边界是什么?有哪些注意事项?

  • GraalVM Native Image 与 ByteBuffer
  • 直接缓冲与堆外内存
  • 反射与 native 支持

GraalVM Native Image 将 Java 应用编译为原生可执行文件,其运行时基于 Substrate VM,与标准 JVM 在内存管理、反射、类加载上有差异。ByteBuffer 在 Native Image 中的边界:直接缓冲(DirectByteBuffer)的堆外内存管理与 Cleaner 机制在 Native Image 中可能不同(Substrate VM 有自己实现的 Cleaner 支持);某些依赖反射或 JNI 的 ByteBuffer 操作需在 native image 配置中注册(如 reflection config);ByteBuffer 的零拷贝/transferTo 等 native 操作在 Native Image 中的支持需验证。注意事项:Native Image 下直接内存的回收、Cleaner 的触发、linux 特异性(如 epoll)的支持等需测试确认。虽然 ByteBuffer 基础功能在 Native Image 中可用,但涉及 Cleaner、反射、JNI 的边界需专门配置与验证。理解 ByteBuffer 与 Native Image 的边界(Cleaner、反射、native 支持)是本题关键。

核心是 ByteBuffer 在 Native Image 中 Cleaner、反射、JNI 等机制与标准 JVM 有差异,需配置与验证。理解边界与注意事项是本题关键。

#

85. JDK 25 中 ServerSocketChannel 配置为非阻塞模式的方法(configureBlocking(false))

JDK 25 中如何将 ServerSocketChannel 配置为非阻塞模式?configureBlocking(false) 的作用是什么?

  • configureBlocking(false) 方法
  • 非阻塞配置的语义
  • 与 Selector 配合

在 JDK 25 中,通过 channel.configureBlocking(false) 将 ServerSocketChannel 配置为非阻塞模式。configureBlocking(false) 会将通道的阻塞状态设为非阻塞,使 accept() 等操作立即返回(无连接时返回 null),而非阻塞等待。配置非阻塞后,通道才能注册到 Selector 进行多路复用(阻塞通道不能注册到 Selector,会抛 IllegalBlockingModeException)。configureBlocking(true) 可恢复阻塞模式。对于 ServerSocketChannel,非阻塞配置后配合 Selector 的 OP_ACCEPT 事件实现高并发 accept。注意:通道处于阻塞状态时不能注册到 Selector,且已注册后不能随意切换阻塞状态。理解 configureBlocking(false) 的作用(非阻塞、可注册 Selector)是本题关键。

核心是 configureBlocking(false) 使通道非阻塞、可注册 Selector,阻塞通道不能注册。理解配置方法与作用及与 Selector 的配合是本题关键。