
在前面的章节中我们聚焦于 Java 并发的核心——线程之间的协作与控制锁、CAS、AQS、CountDownLatch、Semaphore……这些工具让我们能优雅地协调多个线程避免数据错乱与资源争抢。真正的性能瓶颈往往不在 CPU 内部的微秒级竞争而在线程与外部世界交互时的毫秒级等待—— I/O。当线程发起一次数据库查询、读取一个网络数据包、或写入一个文件时它可能要“空等”几十甚至几百毫秒。在这段时间里线程被阻塞无法执行其他任务而系统却不得不为它保留一份宝贵的栈内存和调度开销。这就是“并发真正的战场”不是线程之间抢资源而是如何让线程在 I/O 等待时不再“睡大觉”。接下来我们将正式进入 I/O 编程的世界从最传统的BIO阻塞 I/O开始逐步揭开NIO非阻塞 I/O 多路复用的神秘面纱并最终抵达AIO异步 I/O的前沿。我们还会深入探讨Reactor/Proactor这两种经典的事件驱动架构理解它们如何支撑起高性能服务器如 Netty、Redis、Nginx。同步与异步IO同步IO与异步IO这是理解所有IO模型的基石。两者的区别关键在于发起IO请求后线程是否需要等待IO操作真正完成。同步IO是你发起了IO请求后阻塞等待直到数据完成。异步IO是你发起了IO请求后立刻返回IO完成后再主动通知你。1、同步IO同步IO又可以分为阻塞IO和非阻塞IO区别在于等待的方式不同。1.1、阻塞IOBIO这是最传统的IO模型。InputStream in socket.getInputStream(); byte[] buf new byte[1024]; in.read(buf); // 线程在这里阻塞直到数据完全到达 // 数据到手继续执行特点线程发起read()调用后一直等待直到数据从内核拷贝到用户空间。在此期间线程什么都不做被操作系统挂起。类比你去奶茶店排队站在柜台前一动不动直到奶茶做好递到你手上。1.2、非阻塞IONIO线程发起 IO 请求后不等待立即返回一个状态“数据还没准备好”或“数据准备好了”。SocketChannel channel SocketChannel.open(); channel.configureBlocking(false); // 设置为非阻塞模式 ByteBuffer buf ByteBuffer.allocate(1024); int bytesRead channel.read(buf); // 立即返回 // bytesRead 0 表示还没数据 // bytesRead 0 表示读到了数据特点每次read()调用都立即返回不会阻塞。但如果数据没准备好返回 0线程需要反复轮询自旋检查数据是否就绪。类比你去奶茶店问“好了吗”店员说“还没”你就走了过一会儿再来问一次直到拿到奶茶。问题轮询会浪费 CPU所以通常需要配合IO 多路复用Selector来使用。那这么看来非阻塞IO和异步IO不是一样了吗这里讲一下IO重要的两个过程数据就绪将信息读取到内核/进程缓冲区准备发送数据拷贝发生数据以Java和MySql为例输出IO当你在 Java 里执行一条 SQL 语句时Java 程序需要把这条指令发送给 MySQL。Java应用层 → 内核Socket发送缓冲区 → 网卡 → 网络线路 → MySQL 服务器的网卡 → MySQL内核接收缓冲区输入IOMySQL 处理完你的订单后把查询结果返回给 Java。MySQL内核发送缓冲区 → 网卡 → 网络线路 → Java程序的网卡 → 内核Socket接收缓冲区 → Java应用层所以他们的区别是IO 模型第一阶段数据就绪第二阶段数据拷贝非阻塞 IO不阻塞立即返回但需轮询阻塞线程自己把数据从内核拷贝到用户空间异步 IO不阻塞立即返回不阻塞系统帮你拷贝完然后通知你关键点非阻塞 IO 在数据拷贝阶段仍然是阻塞的而异步 IO 全程不阻塞。做一个生活例子非阻塞 IO你点了一杯奶茶每隔一分钟就去问店员“好了吗”店员说“还没”数据未就绪→ 你走了过一会儿再来问店员说“好了”数据就绪→你站在柜台前等店员把奶茶做好递到你手上数据拷贝阶段你在等你在“轮询”时不阻塞但在“拿到奶茶”的那一刻你还是得等着。异步 IO你在 App 上点奶茶然后去打游戏。奶茶做好了骑手直接送到你家门口按门铃通知你系统主动回调你开门拿奶茶就行你全程没有等过任何人从制作到送达都是系统在帮你搞定。2、异步IOAIOJDK7中引入了AIOAsynchronous IO特点线程发起read()调用后立即返回完全不用等待。当 IO 操作真正完成时系统会主动回调你注册的处理函数。线程在整个过程中从未阻塞可以做其他任务。类比你在 App 上点奶茶然后去打游戏、看电影。奶茶做好了骑手打电话通知你来取。你全程没浪费时间等待。3、架构设计模式Reactor和Proactor3.1、Reactor反应器模式核心思想“来了事件我通知你你来处理”。工作机制服务员Reactor站在前台负责监听所有的客人Socket 连接。当有客人按门铃比如数据可读了服务员立刻通知后厨的厨师Handler。厨师必须亲自走过去把菜端回来应用程序主动调用read()读取数据然后再开始炒菜执行业务逻辑。特点感知的是“就绪事件”告诉你“可以读了”但实际的 I/O 读写操作还是由应用程序自己完成。在 Java 中NIO 就是典型的 Reactor 模式。它通过Selector多路复用器来监听事件然后在事件到来时由工作线程去执行channel.read()。组件角色职责Event事件信号表示“有数据可读”或“可以写数据”Reactor反应器调度员循环等待事件事件来了分发给对应的 HandlerDemultiplexer多路分发器监听器底层调用select()/epoll()阻塞等待事件就绪Dispatcher分发器派单员将就绪的事件分配给对应的 HandlerHandler处理器工人实际执行读写操作和业务逻辑Step 1: Reactor 启动让 Demultiplexer 开始监听所有 Socket Step 2: Demultiplexer 调用 select()阻塞等待 Step 3: 有数据到达select() 返回 Step 4: Dispatcher 拿到就绪的事件列表 Step 5: Dispatcher 把事件分配给对应的 Handler Step 6: Handler 自己调用 channel.read() 读取数据 Step 7: Handler 处理业务逻辑 Step 8: 回到 Step 2继续循环代码解释// Reactor 调度员 Selector selector Selector.open(); // Demultiplexer select() 阻塞等待 while (true) { selector.select(); // 阻塞直到有事件就绪 // Dispatcher 遍历就绪的事件 SetSelectionKey keys selector.selectedKeys(); for (SelectionKey key : keys) { // Handler 处理事件 if (key.isReadable()) { SocketChannel channel (SocketChannel) key.channel(); ByteBuffer buf ByteBuffer.allocate(1024); channel.read(buf); // Handler 自己读数据 // 业务处理... } } }3.2、Proactor前摄器模式核心思想“你告诉我想吃什么我做完了再叫你”。工作机制客人应用程序把菜单和空盘子缓冲区一起交给服务员Proactor。服务员把菜单交给后厨操作系统内核自己就不用管了。后厨做完菜内核完成数据拷贝直接端到客人桌上然后喊一声“饭好了”回调通知。特点感知的是“完成事件”告诉你“已经读完了”I/O 操作数据拷贝完全由操作系统内核完成应用程序只负责处理结果。在 Java 中AIO 就是 Proactor 模式。你调用AsynchronousSocketChannel.read(buffer, attachment, handler)传入缓冲区和处理回调线程立即返回内核读完后自动回调给你。组件角色职责Operation操作任务单告诉系统“我要读/写结果放这里”Proactor前摄器调度员发起异步操作等待完成通知Completion Handler完成处理器回调系统完成 IO 后自动调用的函数Asynchronous Operation Processor异步操作处理器执行者操作系统内核真正执行 IO 操作Completion Dispatcher完成分发器派单员将完成的事件分发给对应的 HandlerStep 1: 你告诉 Proactor“我要读这个 Socket 的数据读完了放到 buffer 里然后调用我的回调函数” Step 2: Proactor 把任务交给操作系统内核 Step 3: 内核开始读数据你把这件事忘了去做别的事 Step 4: 内核读完了数据已经放到了你给的 buffer 里 Step 5: 内核通知 Proactor“读完了” Step 6: Completion Dispatcher 找到你注册的回调函数 Step 7: 回调函数被调用你直接在 buffer 里拿到数据 Step 8: 你处理业务逻辑不需要再做 read()代码解释// 你告诉 Proactor帮我读数据读完了回调我 AsynchronousSocketChannel channel AsynchronousSocketChannel.open(); ByteBuffer buffer ByteBuffer.allocate(1024); // 发起异步读操作传入 buffer 和回调 channel.read(buffer, null, new CompletionHandlerInteger, Void() { Override public void completed(Integer bytesRead, Void attachment) { // 内核已经读完了数据在 buffer 里你直接处理 buffer.flip(); byte[] data new byte[buffer.limit()]; buffer.get(data); System.out.println(读到了: new String(data)); } Override public void failed(Throwable exc, Void attachment) { System.out.println(读取失败); } }); // 这里你可以去做别的事情完全不阻塞 System.out.println(读操作已发起我去干别的了);那为什么我们实际的业务场景中很少看到过他们呢实际上他们都被框架封装了BIO阻塞IO太原始几乎没人直接用NIO非阻塞IO虽然强大但是API很难用需要手动管理Channel、Buffer、Selector还要处理各种异常状态。AIO异步IO回调地狱代码逻辑碎片化也是很难调试比如主流的Netty底层使用的是NIOReactor模式反应器模式它把复杂的NIO细节全都封装起来了只需要简单的几行Handler就可以处理高并发。Tomcat底层也是使用的NIO。在高并发场景下我们的目标是“处理业务逻辑”而不是“手写网络协议栈”。学习 NIO/AIO 的意义在于理解底层原理比如为什么 NIO 能支撑百万连接它是怎么通过 Selector 轮询的而不是为了每天在业务代码里写selector.select()。为什么 Reactor 更常见因为 Proactor在 Linux 上不好使。ReactorLinux 上有epoll非常高效Java NIO 就是用这个。ProactorWindows 上有IOCP天生支持。但 Linux 上没有真正的 AIOJava 的 AIO 在 Linux 上是用epoll模拟的性能还不如 NIO。所以现实中Netty最流行的网络框架用的是Reactor 模式Redis用的是Reactor 模式Nginx用的是Reactor 模式Proactor只在 Windows 上比较常见本章我们从 BIO 的阻塞困境出发一路走到 NIO 的非阻塞革命再到 AIO 的异步未来并深入剖析了 Reactor 与 Proactor 这两种高性能服务器的核心骨架。至此你已经掌握了 I/O 模型的理论根基。下一章我们将把这些理论落地到工业级实战——探究 Netty 为什么能支撑百万级并发它又是如何将 Reactor 模式发挥到极致的。