
1. 从一次面试问答说起这三个IO模型到底在聊什么“AIO、BIO 和 NIO 的区别是什么”——如果你准备过头几个月的Java后端面试这个问题一定不陌生。它几乎是JVM网络编程里最高频的送分题但有意思的是真正能把三者讲透的候选人并不多。大部分人的回答停在“BIO是阻塞的、NIO是非阻塞的、AIO是异步的”这种口诀层面一追问到“为什么Netty不用AIO”“NIO和AIO各自的底层实现是什么”就露馅了。我先给个整体定位BIOBlocking I/O、NIONon-blocking I/O、AIOAsynchronous I/O是Java在不同版本和不同应用场景下提供给开发者的三种I/O处理模型。它们解决的问题不一样设计哲学不一样适配的业务场景也不一样。实际项目中大部分常规Web服务用的是BIO或者说Servlet容器传统的连接处理方式高性能网关、RPC框架底层几乎都是NIO模型而AIO虽然一度被寄予厚望但在Linux平台上的落地表现并不理想反而在Windows上有过一段相对靠谱的实现。这篇文章我不只给你对比表格我会把三种模型的核心机制拆开讲把“阻塞、非阻塞、异步”这几个词背后的线程模型、系统调用、底层数据结构讲清楚再结合面试场景给你一套可以直接背下来、也能扛住追问的回答框架。无论你是准备面试的Java开发还是想搞清楚手头项目的I/O模型选型这篇文章都适用。2. 先把概念拎清楚阻塞、非阻塞与异步之间不是一回事2.1 “阻塞”和“同步”不是一个维度上的概念我在面试别人时发现一个高频误区很多人把“同步/异步”和“阻塞/非阻塞”混在一起一上来就说“NIO是异步的”。这个说法是错的。NIO的全称是Non-blocking I/O它在本质上仍然是同步I/O只是线程不需要一直卡在系统调用上等待数据就绪。要理解AIO和BIO、NIO的区别第一步就是把这四个词拆开。阻塞与非阻塞讨论的是“发起I/O请求的线程在数据还没准备好之前是否会被挂起”。阻塞模式下线程发起read()调用后如果内核缓冲区里没有数据这个线程就进入等待状态直到数据到达才算完非阻塞模式下read()调用会立刻返回如果没有数据就返回一个标志比如-1或者0线程可以去做别的事过一会再来看。同步与异步讨论的是“数据从内核复制到用户缓冲区这一步由谁来完成以及完成之后怎么通知应用程序”。同步I/O里真正的数据拷贝是发生在read()/write()系统调用内部的应用程序主动等这个调用返回异步I/O里应用程序发起aio_read()之后立刻返回内核把数据准备好并且复制到用户缓冲区之后再通过信号或回调函数通知应用“数据已经到位你直接拿来用就行”。所以组合出来是四种模型同步阻塞BIO就是这一类、同步非阻塞NIO、异步阻塞现实中几乎没有这个组合因为异步本身就是配合回调用的、异步非阻塞AIO。BIO、NIO、AIO这三个简称并不是严格按这个四象限划分的Java里它们更多代表了一套完整的API体系和编程范式但搞清楚底层归属之后很多困惑就迎刃而解了。2.2 BIO线程死等数据一对一服务BIO是Java 1.0就有的传统I/O模型。Socket编程里服务端用ServerSocket.accept()接受客户端连接然后为每个连接分配一个线程这个线程从accept()到read()到write()全程阻塞。我来还原一下最经典的服务端代码长什么样ServerSocket serverSocket new ServerSocket(8080); while (true) { Socket socket serverSocket.accept(); // 阻塞在这里直到有客户端连进来 new Thread(() - { InputStream in socket.getInputStream(); BufferedReader reader new BufferedReader(new InputStreamReader(in)); String line; while ((line reader.readLine()) ! null) { // 处理请求 System.out.println(line); } }).start(); }这段代码的问题显而易见如果客户端连接后不发送数据或者发送得很慢读线程就会一直阻塞在readLine()上线程资源被白白占用。一个线程同时只能服务一个连接而JVM默认的线程栈大小是1MB左右即便一台服务器能开的线程数量有一定弹性当连接数上升到几千、上万时线程切换的开销和内存占用就会把进程拖垮。这个模型的好处是简单、可靠、代码直观。对于连接数少、单个连接传输数据量大的场景比如企业内部管理系统、传统数据库连接池BIO反而是最合适的。很多老项目的TCP服务仍然在用BIO不是因为它们落后而是因为业务规模根本不需要上NIO。2.3 NIO一个线程轮询管理大量连接NIO是Java 1.4引入的一套新I/O API核心组件是Channel通道、Buffer缓冲区、Selector选择器。和BIO面向流不同NIO面向缓冲区数据总是从Channel读入Buffer或者从Buffer写入Channel。最关键的是Selector。它底层在Linux上对应epoll机制在Windows上对应select机制作用是让一个线程可以同时监控多个Channel上的I/O事件。当某个Channel上有数据可读、可以写、有新的连接到达时Selector会返回对应的事件集合线程只需要遍历这个集合逐个处理即可。Selector selector Selector.open(); ServerSocketChannel serverChannel ServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(8080)); serverChannel.configureBlocking(false); serverChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); // 阻塞等待直到至少有一个事件就绪 SetSelectionKey keys selector.selectedKeys(); IteratorSelectionKey it keys.iterator(); while (it.hasNext()) { SelectionKey key it.next(); if (key.isAcceptable()) { SocketChannel channel serverChannel.accept(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { // 从channel读取数据到buffer } it.remove(); } }就这样一个线程就能管理成千上万个连接。线程不再死等某一个连接的数据而是空闲时阻塞在selector.select()上事件到来时统一处理。这个模型最大的价值是省线程连接数再多线程数量基本可控系统吞吐量被打通了一个量级。现代高性能网络框架的底层基础就是NIO。Netty的 boss 线程、worker 线程模型本质上就是基于NIO事件循环做扩展的。2.4 AIO内核全部干完活回调通知AIO是Java 7引入的异步I/O模型也叫NIO.2。它的设计目标是更进一步应用程序发起一个异步读操作之后连“监控事件是否就绪”这步都不用管了内核把数据从socket缓冲区复制到用户缓冲区之后直接通过回调或者Future通知应用程序去处理。AsynchronousServerSocketChannel server AsynchronousServerSocketChannel.open(); server.bind(new InetSocketAddress(8080)); server.accept(null, new CompletionHandlerAsynchronousSocketChannel, Void() { Override public void completed(AsynchronousSocketChannel channel, Void attachment) { server.accept(null, this); // 继续接受下一个连接 ByteBuffer buffer ByteBuffer.allocate(1024); channel.read(buffer, null, new CompletionHandlerInteger, Void() { Override public void completed(Integer result, Void attachment) { // 数据已经在内核复制完成后被放到buffer里这里直接处理 } Override public void failed(Throwable exc, Void attachment) { // 处理异常 } }); } Override public void failed(Throwable exc, Void attachment) { // 处理异常 } });这段代码有两个特点一是没有显式的select阻塞二是所有I/O的结果通过CompletionHandler回调送达。从编程范式上说AIO是一种事件驱动的异步回调模型它比NIO更进一步把线程从I/O等待中彻底解放出来。理论上是这样但现实中AIO在Linux平台上的表现让人一言难尽。Linux内核的异步I/O实现io_uring是现代版本早期的AIO基于epoll模拟成熟度不足Java的AIO实现底层在Linux上仍然依赖epoll事件通知本质上没有做到真正的内核级异步性能相比NIO没有明显优势反而因为回调编程复杂度更高、调试更困难导致它在中后端框架中几乎没有被广泛使用。Netty的作者在官方文档里也表达过对AIO的保留态度这也是Netty在Linux平台上仍然坚持NIO模型的重要原因。3. 三个模型的机制拆解从线程模型到底层系统调用的完整对比3.1 线程模型差异一对一、一对多、还是完全不需要等我来把三种模型的线程模型放到一起看这是面试时最直接的答题线索。BIO是“一连接一线程”。建立一个连接就创建一个线程线程内部从头到尾处理这个连接上的所有读写。连接数等于线程数线程数受操作系统资源限制一般几百到几千就到头了。这个模型天然适合短连接、低并发场景因为每个连接的存活时间很短线程复用率虽然低但创建销毁线程的成本分摊下来也还能接受。NIO是“一线程多连接”。一个线程运行着事件循环通过Selector同时管理成千上万个连接。事件到来时才处理没有事件时线程阻塞在select调用上不消耗CPU。这个模型的核心思想是把“连接”和“处理线程”解耦连接只是注册在Selector上的一个事件源处理线程是共享的。AIO是“连接与线程完全无关”。应用程序发出读写请求后立即返回不需要线程去轮询或等待数据就绪后由内核触发回调。如果这时候一定要给它分配一个线程概念那么“回调执行线程”是内核或框架的线程池提供的业务线程本身全程不参与等待。面试官问你“怎么理解线程模型”你直接把这段话梳理清楚回答基本就稳了。3.2 底层系统调用与数据结构差异讲完线程模型我建议你顺带把底层实现提一嘴这会明显拉开和其他候选人的差距。BIO在Linux上走的是传统的read()/write()系统调用配合socket的阻塞模式。进程调用read()后如果数据没有准备好操作系统会把这个进程的状态设为睡眠TASK_INTERRUPTIBLE把它挂到socket的等待队列上直到数据到达或超时。这个过程中CPU被让出去了线程占用的内存大概1MB但是不消耗CPU资源。NIO在Linux上走的是epoll这一组系统调用。程序先把需要监控的文件描述符通过epoll_ctl注册到内核的事件表里然后调用epoll_wait阻塞等待。事件表里只要有任何一个fd就绪epoll_wait就会返回可处理的事件列表。这里有个关键区别epoll返回的是“就绪事件”本身而不需要程序再去遍历所有连接去挨个检查状态时间复杂度从O(n)降到了O(就绪事件数)。这也是epoll在大规模连接场景下远胜select/poll的地方。AIO在Windows上走的是IOCPInput/Output Completion Port这是Windows内核原生的异步I/O实现完成端口会在线程池里调度一个线程来执行完成回调它的设计是完善的。但在Linux上AIO那套系统调用aio_read()对socket的支持并不好Java的AIO实现实际上往Linux上还是会落到epoll那一套把“可读”事件当作“异步完成”的替代信号。所以本质上在Linux上跑Java AIO底层依然是NIO的实现逻辑只是封装层面多了一层回调。这个事实能解释为什么AIO在Linux上性能上不去。我把关键差异整理成一张表面试前可以反复看维度BIONIOAIO全称Blocking I/ONon-blocking I/OAsynchronous I/O引入版本JDK 1.0JDK 1.4JDK 1.7线程模型一连接一线程一线程多连接Selector异步回调不需要业务线程等待阻塞点read/write/accept全阻塞阻塞于selector.select()无阻塞点数据就绪后程序自己去读程序轮询就绪事件后自己读内核复制完成回调直接拿数据底层实现(Linux)read/write阻塞调用epoll事件驱动epoll模拟未真正异步底层实现(Windows)read/write阻塞调用selectIOCP原生异步适用场景连接数少、并发低高并发、连接数多、IO密集型高并发且对异步编程有明确需求的场景编程复杂度简单中等较高代表框架传统Servlet容器Netty、Mina、Tomcat NIO模式早期某些文件I/O场景3.3 缓冲区处理差异流式还是块式BIO的操作单位是字节流你从InputStream里一个字节一个字节读或者用BufferedReader按行读数据在stream里是连续流动的没有边界的概念。这种设计对文本协议比较友好但网络传输中的数据经常是分帧的可能出现半包、粘包问题需要业务代码自己去做边界判断。NIO和AIO的操作单位是Buffer数据在Channel和Buffer之间批量移动。Buffer有很多类型ByteBuffer、CharBuffer、IntBuffer等等其中ByteBuffer用得最多。你在读数据时需要手动控制position当前读写位置、limit有效数据边界、capacity缓冲区容量这三个核心指针读写切换时要调用flip()、clear()、compact()等方法。这个设计比流式处理复杂但性能上限也更高因为数据是按块而不是按字节移动的减少了系统调用次数。举个小例子从Channel读数据到Buffer的典型操作ByteBuffer buffer ByteBuffer.allocate(1024); int bytesRead socketChannel.read(buffer); // 内核把数据拷贝到buffer if (bytesRead 0) { buffer.flip(); // 从写模式切换为读模式 while (buffer.hasRemaining()) { System.out.print((char) buffer.get()); } buffer.clear(); // 清空准备下次写入 }flip()这句看着简单但很多初学者在这里踩坑。如果不调用flip()就直接get()读出来的数据可能是空的或者是不完整的数据因为position还停留在写入的末尾位置读取操作没有从有效数据起始位置开始。4. 面试场景实战如何把回答组织得既有深度又能扛住追问4.1 开场回答概念差异速答面试官问出这个问题的时候他首先想听的是一个干净利落的定义性回答。我建议你按这个顺序说“这三个是Java提供的三种I/O模型。BIO是同步阻塞I/O传统的Socket编程一个连接对应一个线程线程在读写时阻塞并发能力受限于线程数量。NIO是同步非阻塞I/O核心是Channel、Buffer、Selector这三件套一个线程通过Selector管理多个连接数据准备好后线程再去读适合高连接数场景。AIO是异步非阻塞I/O也叫NIO.2应用发起读写后立刻返回内核完成数据复制后通过回调通知应用进一步省掉了线程对I/O事件的等待。在Linux平台上实际落地效果不如NIO所以Netty等主流框架默认不用AIO。”这段话大概40秒把三种模型的定义、核心机制、实际落地差异全说到了。说完之后面试官大概率会顺着往下追问这时候就到了展示深度的时候。4.2 被追问“为什么不推荐AIO”时怎么答这是最容易踩坑的问题。如果你只是简单说“AIO性能不如NIO”面试官会觉得你没认真研究过。正确姿势是把原因拆成两层第一层是Linux内核的异步I/O机制不够成熟。早期的Linux AIO系统调用aio_read()对文件描述符支持有限对socket支持更差后续虽然有io_uring这种现代异步框架但Java的运行时和第三方框架并没有第一时间跟进适配。Java的AIO在Linux上的实现是绕道epoll模拟出来的既然底层还是epoll那一套那它和NIO的性能差距很难拉开还多了一层回调封装的开销和复杂度。第二层是编程模型的复杂性。AIO把控制权完全交给回调代码的执行流程变得不连贯异常处理也被分散到failed方法里一旦业务逻辑复杂调试和排错的成本很高。NIO虽然也复杂但它的代码逻辑至少在同一个线程内是顺序可读的出事之后可以通过日志定位到某一次事件处理的循环里。权衡下来工程团队更愿意选择NIO而不是AIO。4.3 被追问“Netty为什么不用AIO”时怎么答这个问题是上一问的延伸但更贴合实际框架选型。Netty的定位是高性能网络应用框架它要在所有主流操作系统上提供一致的性能表现。在Windows上AIO有IOCP支撑性能确实好但在Linux上AIO名不副实。如果Netty全盘采用AIO就意味着不同平台要走两套底层而且Linux作为服务器端的主力系统反而表现最弱这不符合Netty的跨平台高性能目标。另外还要提一个设计点Netty的线程模型是从Reactor模式演化来的事件循环和NIO的Selector机制天然契合。NIO的事件驱动模型可以很好地配合Netty的pipeline机制在框架层面把编解码、业务处理链路串联起来。AIO的回调模型虽然也能实现pipeline但会把执行线程的调度权交给操作系统线程池框架对线程模型的控制力度减弱反而不利于精细调优。4.4 被追问“BIO、NIO、AIO分别适合什么场景”时怎么答这个问题考的是工程判断力不是背定义。我给的参考回答BIO适合连接数不多、单连接持续传输的场景。典型例子是传统的JDBC连接池连接数可能只有几十到几百每个连接的使用频率高线程阻塞等待数据库响应也不是多大的问题。再比如企业内部的管理后台、设备控制网关这些系统并发量低用BIO代码简单、稳定可靠线上问题也好排查。NIO适合连接数大、单连接请求频率不高或者请求量和连接数不完全匹配的场景。典型例子是网关服务、IM长连接服务、消息推送服务。几万个设备维持一个TCP长连接随时可能有消息进来如果用BIO线程数根本撑不住NIO的一个事件循环线程就能扛住几万个连接的调度。AIO适合对异步编程有明确需求且使用Windows平台的场景或者某些高吞吐的文件I/O场景。比如基于NIO.2的异步文件读写在本地文件复制、日志写入这类场景下AIO的表现比NIO直接轮询要好因为文件I/O的完成时间不确定用回调通知更加自然。但网络编程领域AIO在主流服务器上确实没有站稳脚跟。4.5 面试官常挖的坑accept()和read()的阻塞细节有时候面试官会把问题引到具体方法层面比如“accept()是阻塞的吗那为什么NIO里accept不阻塞”这个问题很多人在回答时语义含糊。我来理清楚BIO的ServerSocket.accept()是阻塞的调用线程会一直等待直到有客户端发起连接。NIO的ServerSocketChannel.accept()默认是非阻塞的但在实际代码中我们通常先把channel注册到Selector上然后调用selector.select()阻塞等待OP_ACCEPT事件。也就是说NIO里的线程并不是被accept()方法阻塞而是被select()方法阻塞这个区别很重要。它意味着线程等待的不再是某一个连接事件而是一批注册好的事件源中任意一个发生变化事件粒度从“连接”细化为“连接、读、写、连接关闭”等具体操作。至于NIO的read操作SocketChannel.read()在非阻塞模式下也会立即返回如果数据没准备好就返回0读到末尾返回-1。处理时要注意区分这两种返回值0表示暂时没有数据-1表示对端关闭了连接逻辑处理完全不同。5. 实操经验手写一个简易NIO长连接服务踩过的坑5.1 从0到1的完整实现思路光说不练假把式面试之前我强烈建议你亲手写一个NIO服务端Demo哪怕只是用来加深理解也值。我来分享一个我去年写过的简易长连接服务包含了服务端接收连接、处理客户端数据的完整逻辑。先搭骨架一个ServerSocketChannel监听端口配置为非阻塞模式注册到Selector上监听OP_ACCEPT事件循环里调用selector.select()等待事件处理可接受事件时把新的SocketChannel注册为OP_READ处理可读事件时从Channel读数据到ByteBuffer。我给出一个可以运行的版本去掉异常处理简化了代码但核心流程完整public class NioServer { public static void main(String[] args) throws IOException { Selector selector Selector.open(); ServerSocketChannel server ServerSocketChannel.open(); server.bind(new InetSocketAddress(9000)); server.configureBlocking(false); server.register(selector, SelectionKey.OP_ACCEPT); System.out.println(server start on 9000); while (true) { selector.select(); // 阻塞直到有事件 IteratorSelectionKey keys selector.selectedKeys().iterator(); while (keys.hasNext()) { SelectionKey key keys.next(); keys.remove(); if (key.isAcceptable()) { SocketChannel channel server.accept(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); System.out.println(new connection: channel.getRemoteAddress()); } else if (key.isReadable()) { SocketChannel channel (SocketChannel) key.channel(); ByteBuffer buffer ByteBuffer.allocate(1024); int len channel.read(buffer); if (len 0) { buffer.flip(); byte[] bytes new byte[buffer.remaining()]; buffer.get(bytes); System.out.println(receive: new String(bytes)); // 简单回显 ByteBuffer writeBuf ByteBuffer.wrap((ack: new String(bytes)).getBytes()); channel.write(writeBuf); } else if (len -1) { // 对端关闭取消key并关闭channel key.cancel(); channel.close(); } } } } } }5.2 我在写这个Demo时踩过的三个大坑第一个坑没有调用keys.remove()或者selectedKeys的清理。selector.selectedKeys()返回的是本次事件的就绪集合处理完一个事件后必须从集合中移除对应的SelectionKey否则下次select()时这个key还会存在于集合里造成重复处理同一个事件轻则重复读数据重则NullPointerException。这是NIO新手最容易犯的问题。第二个坑对OP_READ事件处理中read()返回0的情况处理不当。只要channel配置为非阻塞模式read()返回0是正常现象表示当前没有数据可读。我在早期版本里把返回0也当作异常去关闭连接结果客户端一空闲就被服务端断开。正确的逻辑是len 0就处理数据len -1才关闭连接len 0不做任何事。第三个坑客户端数据半包的处理。上面这个Demo在数据量很小时没问题但当客户端一次性发送超过1024字节的数据时一次read()可能只读走前1024字节剩下数据会在下次select()时继续触发OP_READ。如果业务协议是完整的消息你就需要自己维护一个累积缓冲区把不完整的消息暂存起来等完整帧到达后再解析。这就是Netty里ByteToMessageDecoder要解决的粘包拆包问题实际项目中你不会想自己造的。5.3 用telnet验证服务的正确姿势写完之后怎么验证最简单的办法是用telnet工具连上去手动输入数据telnet 127.0.0.1 9000连上之后随便输入一行字符串按下回车服务端控制台会打印“receive: 你输入的内容”并返回“ack: 你输入的内容”。多开几个telnet窗口你会发现一个服务端线程就能同时处理所有连接的数据。这就是NIO“一个线程管理多连接”最直观的体验。如果你所在环境没有telnet也可以用Python一行命令临时模拟TCP客户端python3 -c import socket;ssocket.socket();s.connect((127.0.0.1,9000));s.send(bhello);print(s.recv(1024))5.4 如果换成AIO实现同一个服务要怎么写我把同样的回显服务用AIO重写了一遍你感受一下编程风格的差异public class AIOServer { public static void main(String[] args) throws IOException, InterruptedException { AsynchronousServerSocketChannel server AsynchronousServerSocketChannel.open(); server.bind(new InetSocketAddress(9001)); System.out.println(aio server start on 9001); server.accept(null, new CompletionHandlerAsynchronousSocketChannel, Void() { Override public void completed(AsynchronousSocketChannel channel, Void attachment) { // 立刻为下一个连接注册accept回调 server.accept(null, this); ByteBuffer buffer ByteBuffer.allocate(1024); channel.read(buffer, null, new CompletionHandlerInteger, Void() { Override public void completed(Integer result, Void attachment) { if (result 0) { buffer.flip(); byte[] bytes new byte[buffer.remaining()]; buffer.get(bytes); String msg new String(bytes); System.out.println(receive: msg); channel.write(ByteBuffer.wrap((ack: msg).getBytes())); buffer.clear(); channel.read(buffer, null, this); // 继续读取下一条消息 } } Override public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); } Override public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); Thread.currentThread().join(); // 主线程等待避免进程退出 } }AIO代码的嵌套层级比NIO深accept成功回调里套read成功回调read回调里套下一次read回调。逻辑上不复杂但一旦业务步骤变多这个回调地狱会非常难看。这也是工程上更愿意用NIO而不用AIO的另一个现实原因代码的可读性和可维护性也是选型的重要考量。6. 常见问题与面试官话术应对实录6.1 “NIO既然是Non-blocking为什么selector.select()还会阻塞”这是我在面试中最喜欢追问的一个问题。很多背题型的候选人在这里会卡壳。答案是NIO的非阻塞指的是“单个Channel上的I/O操作不阻塞”但Selector的select()方法本身是阻塞的否则线程会进入忙轮询状态CPU占用会飙升。线程阻塞在select()上等待的是事件通知这个等待是高效的线程让出CPU由内核在事件发生时唤醒它。从整体上看NIO的网络I/O线程大部分时间都处于阻塞等待事件的状态这和BIO的线程阻塞有本质区别——BIO阻塞时只等一个连接NIO阻塞时等的是所有注册连接的任意事件。6.2 “为什么说AIO是异步的但Java的AIO在Linux上又不算真正的异步”这个问题的核心在于区分“API层面的异步”和“操作系统层面的异步”。Java的AIO API提供的是CompletionHandler回调让你发完请求就不用管了从程序员视角确实是异步编程。但Linux平台上的底层实现并没有走真正的异步I/O系统调用而是用epoll事件机制模拟出来的。真正的异步I/O应该由内核完成数据从内核态到用户态的拷贝然后通知应用直接使用而Java的AIO在Linux上是内核通知“数据可读”应用还得自己调用read()去把数据拷贝出来。所以API是异步的底层仍然是同步I/O的流程。6.3 “Tomcat的BIO和NIO模式差别在哪里”Tomcat从8.5/9.0版本开始已经彻底移除了BIO模式默认是NIO模式。老版本Tomcat的BIO模式里每个HttpServletRequest对应一个线程处理线程从socket读请求行、读请求头、读请求体全程阻塞。连接数一多线程池打满后面请求只能排队。换到NIO模式后Tomcat的Acceptor线程只负责接收连接注册到Poller线程监控事件Poller发现可读事件后把任务丢给工作线程池处理工作线程在处理业务时才占用等待网络数据的工作不再占用线程。这解释了为什么同样一台机器Tomcat从BIO切到NIO后能支撑的连接数能提升一个数量级。6.4 “RPC框架的I/O模型选择有标准答案吗”理论上RPC框架在网络传输层都可以用BIO。但实际的高性能RPC框架比如Dubbo、gRPC这些几乎都基于NIO实现原因在于RPC的调用方通常是海量并发请求TCP连接的复用率极高BIO的“一连接一线程”模式会让线程数失控。Dubbo默认使用的就是Netty底层是NIO事件循环。这里有一个反直觉的点NIO虽然适合高连接数但单个连接上的吞吐量并不一定比BIO高因为NIO要对每个事件做额外调度和状态管理。所以如果你的系统就是单连接、持续大数据传输比如文件传输服务BIO或者专门的传输协议反而可能更高效。6.5 会不会被问到“IO多路复用和NIO的关系”大概率会被追问。IO多路复用是操作系统提供的一种能力让一个线程同时监控多个文件描述符的可读、可写状态select、poll、epoll都是具体的实现机制。NIO在Java层的Selector就是IO多路复用的封装。更严格地说Java NIO的Selector在Linux上就是epoll的包装在Mac上则是kqueue的包装。所以NIO能够做到“一个线程管成千上万连接”本质上是IO多路复用机制在Java层的体现。而BIO完全没有使用多路复用因为它不需要同时监控多个连接——一个线程只盯着一个连接的读事件。AIO在Windows上走IOCP它的逻辑更加接近“异步通知”而不是“多路复用”但在Linux上的Java实现又绕回了epoll所以准确地说AIO的Linux实现也是多路复用的变体。7. 给准备面试的人的最终建议7.1 不要只背结论要把机制画出来我见过很多候选人能把概念表背得滚瓜烂熟但一让画图就露馅。面试前你可以试着在一张纸上画出三种模型的线程与连接关系图BIO是每个线程拉着一根线连接一个socketNIO是一个线程面前放着一个SelectorSelector连着很多socketAIO是socket直接连着回调函数的代码块。如果你能自己画出来说明你是真懂了而不是背了一堆结论。另外要留意我在面试时经常让人现场模拟“假设有个客户端连接上来之后每10秒才发一条消息服务端线程怎么看”的场景。BIO里那个线程这10秒就是在阻塞等数据白白占用资源NIO里线程在等其它事件这个连接只是注册列表里的一项AIO里压根没有线程在等。能把这段场景叙述清楚面试官基本就认可你对三者的理解了。7.2 结合JDK版本演进讲亮点更大有个很讨巧的加分技巧把三种模型放进JDK版本的时间线里讲。JDK 1.0时代刚有网络编程BIO是唯一选择。JDK 1.4引入NIO这一版就是为高性能网络框架准备的但早期的NIO API用起来很繁琐所以Netty这种封装框架才有生存空间。JDK 7引入AIO本意是提供真正的异步I/O文件I/O场景也好、网络I/O场景也好都有更高级的API可用。但后来Linux平台上的网络AIO没有被大规模采用反而NIO经过Netty等框架的深度封装成了事实上的标准。JDK 9又开始推进异步的、基于流的API设计这是更高层面的演进方向。把时间线讲出来说明你在系统性理解这个问题而不只是背了三个名词。7.3 最后提醒写代码永远比背定义管用这篇文章信息量不小但我最想给你留的一句忠告是就算你把所有知识点都背下来了如果不亲手写一遍NIO服务端、不让几个客户端同时连上来实测一把你对这三者的理解始终是空心的。花半小时把上面那个Demo跑起来把telnet打开亲眼看一两个线程扛住几十个连接比你在网上看十篇对比文章都值。面试的时候如果还能把我上面写的那些底层原理结合你跑过的现象来叙述你已经超过绝大多数候选人了。