
BIO (同步阻塞I/O)一个连接对应一个线程。线程在等待数据读写时被“挂起”什么也不做直到数据准备好。NIO (同步非阻塞I/O)一个线程可以管理多个连接。线程会不断地主动询问每个连接“数据准备好了吗”没准备好就去处理下一个。AIO (异步非阻塞I/O)一个线程管理多个连接但不需要主动询问。操作系统会在数据准备好后主动通知应用程序来读取或写回结果。详细区别与对比特性BIONIOAIOI/O模型同步阻塞同步非阻塞异步非阻塞线程模型一连接一线程一请求一线程 (使用Selector多路复用器)有效请求一线程 (依赖操作系统回调)应对并发并发能力弱线程资源开销大并发能力强适合海量连接并发能力强尤其适合重I/O操作编程复杂度简单易理解复杂需要理解Selector、Buffer、Channel中等需要理解回调或Future数据读取线程阻塞直到数据完全读完主动轮询数据分批次读入BufferOS完成读写后回调通知程序处理适用场景连接数少、并发低、同步操作的场景连接数多、连接时间短聊天、即时通讯、高负载场景连接数多、连接时间长、数据量大文件、图片、音视频的场景档案系统中的 NIO 使用场景档案系统的核心需求是海量文件PDF、图片、音视频等的上传与下载、文档预览、关键词检索、版本管理等。用户数量多并发请求密集且很多请求需要快速响应。1. 档案文件的高速下载服务零拷贝传输场景描述很多用户同时下载不同的大容量档案如一份扫描的蓝图 50MB、一段庭审录音 100MB。传统 BIO 会把文件从磁盘读到 JVM 堆内存再拷贝到 Socket 发送缓冲区导致多次内存拷贝和 CPU 占用。NIO 如何解决利用FileChannel.transferTo()或transferFrom()方法直接在操作系统层面实现“零拷贝”DMA 直接发送到网卡缓冲区避免用户态与内核态之间的多余数据复制。好处大幅提升文件传输速度降低 CPU 和内存占用支撑数千并发下载。2. 档案预览/缩略图请求的 I/O 多路复用场景描述用户在档案管理页面滚动列表每条记录需要显示缩略图几百 KB。前端可能同时发起几十个图片请求服务器需要快速打开多个小文件并返回。NIO 如何解决用一个 Selector 管理所有图片请求的SocketChannel单线程轮询哪些通道已就绪可读/可写批量处理。避免为每个请求创建单独线程BIO 下会快速耗尽线程池。好处支持高并发预览请求页面加载流畅服务器线程资源稳定可控。电商系统中的 NIO 使用场景电商系统的特点是瞬时高并发如秒杀、双11、请求短平快、需要低延迟响应商品详情、加购物车、下单。现代电商网关和应用服务器如 Spring Cloud Gateway、Netty普遍基于 NIO。1. 商品详情页的异步聚合请求网关层场景描述一个商品详情页需要从多个微服务库存、价格、促销、评论、图片拉取数据。如果每个服务调用都阻塞等待总延迟会叠加且每个请求独占一个线程。NIO 如何解决在网关或 BFF 层使用 NIO 的非阻塞 HTTP 客户端如 WebClient、Netty 的 HttpClient并发发出多个请求然后通过Selector等待所有响应返回一旦某个响应先回来就立即处理。整个过程只需要少量线程。好处显著减少单次请求的整体耗时用更少的线程支撑每秒数万请求。2. WebSocket 客服聊天/订单实时推送场景描述电商平台需要为用户提供客服即时通讯、订单状态实时推送“您的包裹已发货”。WebSocket 长连接可能同时存在数十万个。NIO 如何解决每个 WebSocket 连接对应一个SocketChannel注册到同一个Selector上由单一线程负责监听所有通道的消息到达事件。当某个用户发来聊天消息时该线程唤醒并处理然后继续轮询。好处单机可以轻松支撑数十万 WebSocket 长连接而不是 BIO 模式下几万个连接就耗光内存。3. 秒杀系统的请求过滤准入层场景描述秒杀瞬间涌入数百万请求如果全部通过 Tomcat 线程池会直接压垮数据库。需要一个前置的“阀门”快速过滤无效请求比如只让前 1000 个请求进入业务逻辑。NIO 如何解决基于 NIO 实现一个简单的非阻塞网关只读取 HTTP 请求头中的用户 ID、商品 ID用内存计数器判断是否放行。对于被拒绝的请求直接返回“秒杀结束”的响应全程非阻塞。好处单机可处理百万级 QPS 的请求检查且不消耗大量线程有效保护后端系统。总结为什么这两个系统偏爱 NIO档案系统大量文件 I/O 高并发连接NIO 的零拷贝和多路复用让磁盘到网络的流转更高效。电商系统瞬时高并发 短连接 长连接WebSocketNIO 用少量线程管理海量连接完美匹配电商大促场景。NIO 核心原理解析NIONon-blocking I/O基于Reactor 模式核心组件包括Channel通道双向数据传输的管道可读写如FileChannel、SocketChannel、ServerSocketChannel。Buffer缓冲区一块内存区域用于暂存数据。所有数据通过 Buffer 与 Channel 交互。Selector选择器多路复用器可监听多个 Channel 的事件连接、读、写用一个线程轮询就绪事件实现单线程管理大量连接。Buffer缓冲区NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中而写操作时将 Buffer 中的数据写入到 Channel 中。Channel通道Channel 是一个双向的、可读可写的数据传输通道NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念它可以代表文件、套接字或者其他数据源之间的连接。Selector选择器允许一个线程处理多个 Channel基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上由 Selector 来分配线程来处理事件。工作流程将ServerSocketChannel设置为非阻塞并注册到Selector感兴趣事件为OP_ACCEPT。Selector.select()阻塞等待就绪事件。有连接请求时ServerSocketChannel.accept()得到SocketChannel也设为非阻塞注册到同一个Selector监听OP_READ。当有数据可读时Selector返回线程处理OP_READ事件从SocketChannel读取数据到Buffer解码、处理业务将结果写入Buffer并发送回客户端。零拷贝档案系统关键点FileChannel.transferTo()利用操作系统的 DMA 机制直接将文件数据从磁盘发送到网卡缓冲区避免用户态与内核态之间的多次拷贝大幅提升文件传输性能。用档案系统举个例子服务端只有一个线程不断调用selector.select()。事件1000 个用户同时在下载大档案。每建立一个新的下载连接就把对应的SocketChannel注册到Selector并设置感兴趣事件为OP_READ等待用户请求文件名。处理当某个用户发来下载请求读事件线程通过FileChannel.transferTo()直接把磁盘文件发回给客户端这个过程不需要把数据读到 Java 堆里零拷贝。发完后关闭该通道继续轮询其他事件。整个过程一个线程就能稳定支撑上千个并发下载CPU 几乎不因为线程切换而浪费。档案系统零拷贝文件下载服务场景多个客户端并发请求下载归档文件如 PDF。服务端使用 NIO 零拷贝传输。java// 档案下载服务器 – 使用零拷贝 public class ArchiveServer { // 声明公共类 ArchiveServer代表一个档案文件服务器 public static void main(String[] args) throws IOException { // 程序主入口抛出 IOException 以简化代码中的 I/O 异常处理 ServerSocketChannel serverChannel ServerSocketChannel.open(); // 打开一个服务端 Socket 通道用于监听客户端的 TCP 连接请求 serverChannel.socket().bind(new InetSocketAddress(8080)); // 将服务器通道绑定到本机的 8080 端口 serverChannel.configureBlocking(false);//配置非阻塞 // 将通道设置为非阻塞模式这是 NIO 的核心使得 accept() 等操作不会阻塞当前线程 Selector selector Selector.open(); // 打开一个选择器它是一个多路复用器用于监控多个通道的 I/O 事件 serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 将服务端通道注册到选择器上并监听 OP_ACCEPT 事件即客户端连接事件 while (true) { // 进入无限循环持续处理客户端请求 selector.select(); // 阻塞直到有事件 // 这是一个阻塞方法直到至少有一个注册的事件如连接、读就绪发生才会返回 IteratorSelectionKey keys selector.selectedKeys().iterator(); // 获取所有已就绪的事件集合并获取其迭代器 while (keys.hasNext()) { // 遍历所有就绪的事件 SelectionKey key keys.next(); // 获取当前就绪的事件对象 keys.remove(); // 从集合中移除当前事件。这非常重要如果不移除下次循环会重复处理该事件 if (key.isAcceptable()) { // 判断该事件是否为客户端连接就绪事件 SocketChannel clientChannel serverChannel.accept(); // 接受客户端连接返回与客户端通信的 Socket 通道 clientChannel.configureBlocking(false); // 将客户端通道也设置为非阻塞模式 clientChannel.register(selector, SelectionKey.OP_READ); // 将客户端通道注册到选择器上并监听 OP_READ 事件即客户端发送数据的事件 } else if (key.isReadable()) { // 判断该事件是否为客户端数据读取就绪事件 handleClientRequest(key); // 调用自定义方法处理客户端的读取请求 } } } } private static void handleClientRequest(SelectionKey key) throws IOException { // 定义处理客户端请求的方法传入触发事件的 SelectionKey SocketChannel clientChannel (SocketChannel) key.channel(); // 通过 key 获取对应的客户端通道 ByteBuffer buffer ByteBuffer.allocate(1024); // 分配一个 1024 字节的缓冲区用于读取客户端发送的数据 int read clientChannel.read(buffer); // 将客户端通道的数据读取到缓冲区中返回读取的字节数 if (read -1) { // 如果返回 -1表示客户端已正常关闭连接 clientChannel.close(); // 关闭客户端通道 return; // 结束当前方法 } buffer.flip(); // 翻转缓冲区。将缓冲区的极限设为当前位置位置设为 0为后续从缓冲区读取数据做准备 String request Charset.defaultCharset().decode(buffer).toString(); // 使用默认字符集将缓冲区中的字节解码为字符串即客户端发送的 HTTP 请求头 // 解析请求假设格式: GET /archive/123.pdf // 注释说明假设请求的第一行格式如上所示 String fileName request.split( )[1]; // 按空格分割请求字符串取第二部分作为请求的文件名例如/archive/123.pdf Path filePath Paths.get(/data/archives, fileName); // 拼接文件在服务器磁盘上的绝对路径基础路径为 /data/archives if (Files.exists(filePath)) { // 判断请求的文件在服务器上是否存在 // 零拷贝核心transferTo // 注释说明接下来使用零拷贝技术进行文件传输 FileChannel fileChannel FileChannel.open(filePath); // 打开文件的 FileChannel用于读取文件 long size fileChannel.size(); // 获取文件的大小字节数 // 响应头简单 HTTP 响应 // 注释说明构建一个简单的 HTTP 响应头 String header HTTP/1.1 200 OK\r\nContent-Length: size \r\n\r\n; // 构建响应头字符串包含状态码 200 OK 和内容长度 Content-Length以 \r\n\r\n 结尾分隔头部和正文 ByteBuffer headerBuffer ByteBuffer.wrap(header.getBytes()); // 将响应头字符串转换为字节数组并包装成一个 ByteBuffer clientChannel.write(headerBuffer); // 将响应头写入客户端通道发送给客户端 // 零拷贝发送文件内容 // 注释说明使用零拷贝方式发送文件正文 long transferred fileChannel.transferTo(0, size, clientChannel); // 零拷贝核心代码将文件通道中从 0 开始到 size 长度的数据直接传输到客户端通道。 // 底层使用操作系统的 sendfile 系统调用数据直接在内核空间从文件缓冲区传输到网卡避免了用户空间的拷贝 System.out.println(Transferred transferred bytes for fileName); // 打印传输日志记录传输的字节数和文件名 fileChannel.close(); // 关闭文件通道释放系统资源 } else { // 如果文件不存在 String error HTTP/1.1 404 Not Found\r\n\r\n; // 构建 404 错误的 HTTP 响应头 clientChannel.write(ByteBuffer.wrap(error.getBytes())); // 将 404 响应写入客户端通道 } clientChannel.close(); // 处理完毕后关闭客户端通道在简单的 HTTP/1.0 或短连接场景下适用 } }说明FileChannel.transferTo()直接将文件数据发送到SocketChannel无需将数据读入 Java 堆内存。适合大文件、高并发下载CPU 和内存开销极低。