示例环境JDK 17.0.12Windows 11。三个 TCP 回显示例只使用 JDK 标准库均已在本地完成编译和回显验证。很多人第一次背 BIO、NIO、AIO记住的是三句话BIO 一连接一线程NIO 用一个线程处理多个连接AIO 是异步的所以 AIO 最快。这三句话有对有错。它们描述了三种常见使用方式却把不同层面的概念混在了一起。先给结论BIO的典型模式是一个线程在accept、read或write上等待到操作完成。NIO的核心是通过Channel、Buffer和Selector获取一批连接的“就绪事件”再用少量线程处理事件。AIO的核心是提交 I/O 请求后立即返回操作完成时由回调或Future通知应用。它们不是“旧、新、更新”的简单替换也没有脱离场景的绝对性能排名。下面先把术语拆开再看三套最小代码。一、阻塞和同步说的不是一回事理解三种模型前必须先分清两组词。维度它回答的问题典型判断方式阻塞与非阻塞调用线程会不会一直等结果方法是否会占住当前线程同步与异步调用方怎样获得最终结果当前调用直接返回结果还是稍后收到完成通知阻塞 I/OBlocking I/O的调用线程会停在原地直到操作完成或失败。例如InputStream.read()没有读到数据时调用线程通常会等待。非阻塞 I/ONon-blocking I/O在操作暂时无法完成时会立即返回。例如非阻塞SocketChannel.read()暂时没有数据时可以返回0由调用方决定下一步做什么。同步 I/OSynchronous I/O强调调用方参与结果获取。它可能一直等待也可能轮询或选择就绪事件但最终结果由当前调用链路处理。异步 I/OAsynchronous I/O提交请求后调用方不站在旁边等数据搬运完成。系统在操作完成后调用回调或者让Future进入完成状态。所以会出现两个容易混淆的事实NIO 中的Selector.select()本身可以阻塞但read()和write()通常以非阻塞方式处理已经就绪的连接。AIO 看起来没有阻塞不代表系统里没有线程。操作系统或 JDK 仍然要安排线程、事件或内核机制搬运数据只是这部分不由业务线程直接等待。一句话概括就是阻塞与非阻塞看“线程是不是卡在这里”同步与异步看“最终结果怎样回到调用方”。二、BIO一个线程跟着一次 I/O 走BIO 通常指传统java.io流模型以及基于ServerSocket、Socket的经典网络编程方式。它最直接的写法是主线程在ServerSocket.accept()等待新连接。每接收一个连接把连接交给一个线程或线程池任务。处理线程在read()上等待数据读完后调用write()回写。下面的服务端会把客户端发送的内容原样返回importjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.net.ServerSocket;importjava.net.Socket;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassBioEchoServer{privatestaticfinalintPORT9001;publicstaticvoidmain(String[]args)throwsIOException{ExecutorServiceworkersExecutors.newFixedThreadPool(10);try(ServerSocketservernewServerSocket(PORT)){System.out.println(BIO echo server listening on PORT);while(true){Socketsocketserver.accept();workers.submit(()-handle(socket));}}finally{workers.shutdown();}}privatestaticvoidhandle(Socketsocket){try(socket;InputStreaminsocket.getInputStream();OutputStreamoutsocket.getOutputStream()){byte[]buffernewbyte[1024];intlength;while((lengthin.read(buffer))!-1){out.write(buffer,0,length);out.flush();}}catch(IOExceptione){System.err.println(BIO connection error: e.getMessage());}}}这里有两个等待点server.accept()等新连接。in.read(buffer)等客户端发数据。使用固定大小线程池时正在处理的连接数受线程数限制。连接大量存在但多数时间没有数据时线程会停在read()上内存和线程切换成本都会增加。BIO 的优点是代码直观、调试简单、顺序逻辑容易理解。连接数量不多或者请求处理时间较短时它并没有什么问题。三、NIO用一个 Selector 看一批连接是否就绪Java NIO 的核心抽象不是单一的“非阻塞”而是三件套Channel双向数据通道。Buffer读写数据时使用的缓冲区。Selector集中观察多个通道的事件。Selector关心的不是“这个连接已经完成了整段业务”而是通道是否出现了可接受、可读、可写等事件。应用线程拿到就绪事件后再去执行实际读写。下面是一个最小 NIO 回显服务importjava.io.IOException;importjava.net.InetSocketAddress;importjava.nio.ByteBuffer;importjava.nio.channels.SelectionKey;importjava.nio.channels.Selector;importjava.nio.channels.ServerSocketChannel;importjava.nio.channels.SocketChannel;importjava.util.Iterator;publicclassNioEchoServer{privatestaticfinalintPORT9002;publicstaticvoidmain(String[]args)throwsIOException{try(ServerSocketChannelserverServerSocketChannel.open();SelectorselectorSelector.open()){server.bind(newInetSocketAddress(PORT));server.configureBlocking(false);server.register(selector,SelectionKey.OP_ACCEPT);System.out.println(NIO echo server listening on PORT);ByteBufferbufferByteBuffer.allocate(1024);while(true){selector.select();IteratorSelectionKeykeysselector.selectedKeys().iterator();while(keys.hasNext()){SelectionKeykeykeys.next();keys.remove();if(!key.isValid()){continue;}if(key.isAcceptable()){accept(server,selector);}elseif(key.isReadable()){read((SocketChannel)key.channel(),buffer);}}}}}privatestaticvoidaccept(ServerSocketChannelserver,Selectorselector)throwsIOException{SocketChannelclientserver.accept();if(clientnull){return;}client.configureBlocking(false);client.register(selector,SelectionKey.OP_READ);}privatestaticvoidread(SocketChannelclient,ByteBufferbuffer){buffer.clear();try{intlengthclient.read(buffer);if(length-1){client.close();return;}if(length0){return;}buffer.flip();while(buffer.hasRemaining()){if(client.write(buffer)0){thrownewIOException(outbound buffer is full);}}}catch(IOExceptione){System.err.println(NIO connection error: e.getMessage());close(client);}}privatestaticvoidclose(SocketChannelclient){try{client.close();}catch(IOExceptionignored){}}}这段代码的关键不在while (true)而在selector.select()。连接建立后服务端把它注册为OP_READ。只有当某个通道出现可读事件时代码才会调用对应连接的read()。于是一个事件循环线程可以观察大量连接而不需要为每个空闲连接保留一个等待线程。不过NIO 也带来了更多状态一次read()不一定恰好读到一条完整业务消息需要自己处理半包和粘包。一次write()也不一定把缓冲区全部写完生产实现通常要为连接维护待写队列和OP_WRITE。编码器、解码器、心跳、超时、断线重连和背压都需要业务层或框架层处理。因此NIO 的收益主要是“少量线程管理大量连接”代价是编程和状态管理复杂度更高。四、AIO提交请求完成后再回调AIO 是日常交流中的叫法在 Java 中对应 NIO.2 提供的异步通道例如AsynchronousServerSocketChannelAsynchronousSocketChannelAsynchronousFileChannel这些类位于java.nio.channels并没有一个叫作java.aio的包。理解 AIO 时应该抓住“操作完成通知”不要把类名和某个固定内核实现绑死。异步通道有两种取结果方式Future和CompletionHandler。下面的服务使用回调连续读取客户端数据并回写importjava.io.IOException;importjava.net.InetSocketAddress;importjava.nio.ByteBuffer;importjava.nio.channels.AsynchronousServerSocketChannel;importjava.nio.channels.AsynchronousSocketChannel;importjava.nio.channels.CompletionHandler;publicclassAioEchoServer{privatestaticfinalintPORT9003;privatestaticfinalintBUFFER_SIZE1024;publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{AsynchronousServerSocketChannelserverAsynchronousServerSocketChannel.open();server.bind(newInetSocketAddress(PORT));System.out.println(AIO echo server listening on PORT);acceptNext(server);Thread.currentThread().join();}privatestaticvoidacceptNext(AsynchronousServerSocketChannelserver){server.accept(null,newCompletionHandlerAsynchronousSocketChannel,Void(){Overridepublicvoidcompleted(AsynchronousSocketChannelclient,Voidattachment){acceptNext(server);readNext(client,ByteBuffer.allocate(BUFFER_SIZE));}Overridepublicvoidfailed(Throwableerror,Voidattachment){error.printStackTrace();}});}privatestaticvoidreadNext(AsynchronousSocketChannelclient,ByteBufferbuffer){buffer.clear();client.read(buffer,buffer,newCompletionHandlerInteger,ByteBuffer(){Overridepublicvoidcompleted(Integerlength,ByteBufferdata){if(length0){close(client);return;}if(length0){readNext(client,data);return;}data.flip();writeFully(client,data);}Overridepublicvoidfailed(Throwableerror,ByteBufferdata){close(client);}});}privatestaticvoidwriteFully(AsynchronousSocketChannelclient,ByteBufferbuffer){client.write(buffer,buffer,newCompletionHandlerInteger,ByteBuffer(){Overridepublicvoidcompleted(Integerwritten,ByteBufferdata){if(data.hasRemaining()){writeFully(client,data);}else{readNext(client,ByteBuffer.allocate(BUFFER_SIZE));}}Overridepublicvoidfailed(Throwableerror,ByteBufferdata){close(client);}});}privatestaticvoidclose(AsynchronousSocketChannelclient){try{client.close();}catch(IOExceptionignored){}}}三个类放入src目录后可以用下面的命令编译和启动其中一个服务javac-encoding UTF-8-Xlint:all-d out src\*.java java-cpout BioEchoServer把类名依次换成NioEchoServer和AioEchoServer端口分别为9002和9003。本机客户端发送hello后三种服务都返回hello。这里的accept、read和write都会立即返回一个待完成的操作。完成后JDK 在相应的线程上下文中回调completed失败时回调failed。从业务代码看等待被移除了从整个系统看线程、事件循环或内核异步机制仍然存在。此外AIO 的底层实现与操作系统和 JDK 实现有关。Windows 的异步 I/O 可以依托 IOCP 等完成机制在部分 Unix/Linux 平台上socket 异步通道也可能由 JDK 内部的事件和线程机制支撑。API 层是异步的不代表所有平台都使用完全相同的内核异步 I/O 路径。五、把三种模型放在一张表里维度BIONIOAIO核心抽象流、SocketChannel、Buffer、Selector异步 Channel、回调、Future等待位置当前线程等待一次 I/O 完成Selector 等待一批连接就绪调用立即返回完成后通知编程模型同步阻塞为主同步非阻塞、I/O 多路复用为主异步完成通知常见线程模型一连接一线程或线程池少量事件线程加工作线程回调线程或异步线程池代码复杂度低较高较高且回调链路更分散典型场景连接少、逻辑简单大量空闲连接、长连接服务支持良好的异步文件或网络场景主要风险线程数量和上下文切换半包、背压和状态管理平台差异、调试和生态成熟度常见框架或机制Servlet 阻塞模型、普通 SocketNetty、Selector、epollNIO.2 异步通道、IOCP需要注意表格描述的是典型模型不是语言级规则。“一连接一线程”是 BIO 最常见的组织方式不代表所有 BIO 代码都必须这样做NIO 也不需要每个项目都手写SelectorNetty 等框架会封装事件循环和缓冲区管理。六、几个常见误区1. NIO 一定比 BIO 快吗不一定。连接少、消息处理简单时BIO 的线程模型更直观额外抽象更少。NIO 的优势通常出现在连接多、连接活跃度不均衡、需要长连接复用的场景。“更快”至少涉及吞吐量、延迟、连接规模和资源占用不能只根据 API 类型下结论。2. 非阻塞就是一次调用立刻完成吗不是。非阻塞只表示暂时做不了时不会把线程一直占住。Selector.select()仍会等待事件read()返回0也只表示当前没有读到数据。3. AIO 一定优于 NIO 吗不一定。异步 API 可以减少业务线程的等待但会引入回调拆分、上下文传递、资源生命周期和平台差异。一个模型是否合适取决于操作系统支持、协议复杂度、团队维护方式和框架生态。4. 用了 Netty就等于直接使用 NIO 吗不能这样简单等同。Netty 建立在 Java NIO 等能力之上同时封装了事件循环、ByteBuf、编解码器、连接管理和背压。业务开发面对的是框架抽象不是裸Selector。5. 异步模型没有线程吗有。异步减少的是业务调用线程的等待并不消灭数据搬运和事件处理所需的工作。理解资源成本时应看整个 JDK、操作系统或框架的线程和事件模型。七、实际项目里怎么选可以先按下面的顺序判断连接数少、协议简单、希望快速上线优先使用 BIO 或现成的阻塞式框架。连接数多、长连接多、空闲连接占比高考虑 NIO并优先评估 Netty 等成熟框架。已有平台强依赖完成通知且文件或网络操作适合异步化可以评估 AIO但要先验证目标操作系统、JDK 版本和现有框架的支持情况。普通 Web 业务通常不需要手写 I/O 模型。先看 Spring MVC、WebFlux、Tomcat、Netty 或公司基础框架已经提供了什么。我会把选择标准概括成四个问题同时存在多少连接其中多少连接经常活跃业务逻辑是短请求还是长连接上的持续事件团队能否维护非阻塞状态机或异步回调链路目标平台和框架是否已经验证过对应的 I/O 模型如果答案还不清楚不要先追求“更高级”的模型。先写清楚协议和连接行为再做压测和资源观察。八、面试可以继续追问什么准备面试时可以沿着下面三个方向继续深挖select、poll和epoll在事件通知方式上有什么差异为什么 Netty 在高并发网络服务中常用 NIO 风格的事件循环而不是把所有业务都交给 AIO 回调Java AIO 的Future和CompletionHandler各适合怎样的调用链回调中为什么不能随意阻塞回答时先讲线程模型和资源边界再谈性能。性能数字只有在明确连接数、消息大小、机器配置和测试方法后才有意义。总结总结一下BIO 的典型模式是线程跟着一次阻塞 I/O 走代码简单但连接数增长时线程成本明显。NIO 通过 Channel、Buffer 和 Selector 管理一批连接的就绪事件适合大量长连接但状态管理更复杂。AIO 通过回调或 Future 获得完成通知业务线程不再直接等待操作完成但底层实现和平台生态需要单独评估。三者没有脱离场景的绝对优劣。选型应同时考虑连接规模、协议复杂度、操作系统、框架生态和维护成本。实际选型时我会先判断连接行为和团队维护边界再决定是否需要 NIO 或 AIO。普通业务优先复用成熟框架只有明确需要控制线程模型或事件链路时才深入裸 API。参考Java 17 java.ioJava 17 java.nioJava 17 SelectorJava 17 AsynchronousSocketChannel标签Java、NIO、网络编程、后端开发、并发编程