当前位置: 首页 > news >正文

详解BIO,NIO,AIO

        在理解BIO,NIO,AIO之前,需要先理解同步、异步、阻塞、非阻塞概念。详解同步、异步、阻塞、非阻塞

一、BIO (Blocking I/O - 阻塞式 I/O) - JDK 1.0

  • 模型本质: 同步阻塞模型 (最符合直觉的传统模型)。

  • 工作原理:

    • 当应用线程调用 InputStream.read(), OutputStream.write(),ServerSocket.accept(), Socket.read() 等 I/O 方法时。

    • 该线程会一直被阻塞,直到:

      • 数据准备好可以读取 (对于读操作)。

      • 数据完全写入内核缓冲区 (对于写操作)。

      • 新的客户端连接到达 (对于 accept())。

    • 在阻塞期间,该线程不能执行任何其他任务,CPU 时间片被浪费。

  • 编程模型:

    • ServerSocket + Socket + 线程池: 为了解决一个连接阻塞一个线程导致的资源耗尽问题,通常使用线程池(如 ExecutorService)为每个新建立的客户端连接分配一个线程。

    • 伪代码示例 (服务器端):

      ExecutorService threadPool = Executors.newFixedThreadPool(100); // 假设最大100连接
      try (ServerSocket serverSocket = new ServerSocket(8080)) {while (true) {Socket clientSocket = serverSocket.accept(); // 阻塞等待新连接threadPool.execute(() -> handleClient(clientSocket)); // 为新连接分配线程处理}
      }
      void handleClient(Socket socket) {try (InputStream in = socket.getInputStream();OutputStream out = socket.getOutputStream()) {byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = in.read(buffer)) != -1) { // 阻塞等待客户端数据// 处理数据...out.write("Response".getBytes()); // 可能阻塞写入}} catch (IOException e) { ... }
      }
  • 优点:

    • 编程模型简单直观,易于理解和调试。

  • 缺点 (致命问题):

    • 线程资源消耗巨大: 每个连接需要一个独立线程。线程创建、销毁、上下文切换开销高。

    • 并发能力受限: 受限于线程池大小和操作系统线程数。当连接数(如 C10K 问题)远大于可用线程数时,新连接会被拒绝或严重排队延迟。

    • 资源利用率低: 线程大部分时间在阻塞等待 I/O,CPU 闲置。

  • 适用场景: 连接数少且固定的简单应用,开发速度优先的场景。不适用于高并发网络服务器。

二、NIO (Non-blocking I/O / New I/O) - JDK 1.4 (2002)

  • 模型本质: 同步非阻塞模型 (核心) + 多路复用 (关键优化)。有时也被称为“事件驱动”或“Reactor模式”。

  • 核心组件:

    • Channel (通道): 替代 BIO 中的 InputStream/OutputStream。双向通道,可读可写。关键实现:SocketChannelServerSocketChannelFileChannel核心特性:可配置为非阻塞模式 (configureBlocking(false))。

    • Buffer (缓冲区): 数据容器。NIO 操作的核心是面向 Buffer 进行读写 (ByteBufferCharBuffer 等)。

    • Selector (选择器/多路复用器): NIO 的灵魂。一个线程可以同时监控多个 Channel 的 I/O 事件 (如连接就绪 OP_ACCEPT, 读就绪 OP_READ, 写就绪 OP_WRITE)。应用线程阻塞在 Selector.select() 上,当有事件发生时,select() 返回,应用可以获取到发生事件的 Channel 集合进行处理。

  • 工作原理 (核心流程 - 单线程处理多连接):

    • 创建 Selector

    • 创建 ServerSocketChannel,绑定端口,设置为非阻塞模式。

    • 将 ServerSocketChannel 注册到 Selector 上,关注 OP_ACCEPT 事件 (新连接到达)。

    • 主线程循环调用 Selector.select()该方法会阻塞,直到至少有一个注册的 Channel 有感兴趣的事件发生

    • select() 返回后,获取 SelectionKey 集合 (代表发生事件的 Channel)。

    • 遍历 SelectionKey

      • 如果是 OP_ACCEPT:调用 ServerSocketChannel.accept() (非阻塞,立即返回!) 获取新的 SocketChannel。将新 SocketChannel 设置为非阻塞模式,并注册到同一个 Selector 上,关注 OP_READ 事件。

      • 如果是 OP_READ:获取对应的 SocketChannel,读取数据到 Buffer 进行处理。读取时使用 channel.read(buffer) (非阻塞,可能返回0)。处理完可能需要关注 OP_WRITE

      • 如果是 OP_WRITE:获取对应的 SocketChannel,将响应数据写入 Buffer,然后调用 channel.write(buffer) (非阻塞,可能只写入部分数据)

    • 处理完事件后,移除已处理的 SelectionKey 或改变其关注的事件集。返回步骤 4。

  • 伪代码示例 (服务器端核心循环):

    ​
    Selector selector = Selector.open();
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    serverChannel.bind(new InetSocketAddress(8080));
    serverChannel.configureBlocking(false); // 非阻塞
    serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 关注Acceptwhile (true) {int readyChannels = selector.select(); // 阻塞,等待事件if (readyChannels == 0) continue;Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();keyIterator.remove(); // 必须移除!if (key.isAcceptable()) {// 处理新连接SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();clientChannel.configureBlocking(false);clientChannel.register(selector, SelectionKey.OP_READ); // 关注Read} else if (key.isReadable()) {// 处理读SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = clientChannel.read(buffer); // 非阻塞读if (bytesRead > 0) {buffer.flip();// ... 处理数据 ...// 可能需要改为关注OP_WRITE来写响应key.interestOps(SelectionKey.OP_WRITE);} else if (bytesRead < 0) {// 连接关闭clientChannel.close();}} else if (key.isWritable()) {// 处理写SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer response = ... // 准备响应数据clientChannel.write(response); // 非阻塞写// 如果写完了,可以改回关注OP_READif (!response.hasRemaining()) {key.interestOps(SelectionKey.OP_READ);}}}
    }​
  • 优点:

    • 高并发能力: 单线程即可管理大量连接 (Selector 功劳)。突破了 BIO 的线程数瓶颈。

      • 问题根源
        BIO中每个连接必须独占1个线程。1万个连接就要1万个线程 → 线程切换开销压垮CPU。

      • NIO解决方案

      • Selector像总控台:调用selector.select()时,该线程会阻塞,但内核会监控所有注册的连接

      • 事件驱动:当任意连接有数据到达/可写/新连接时,操作系统唤醒Selector线程,并返回就绪的连接列表

      • 单线程处理多连接:只需遍历就绪的连接处理,处理完继续阻塞监听

      •  关键突破:用1个线程的阻塞(select())代替了N个线程的阻塞,连接数不再受线程数限制。

    • 资源利用率高: 线程只在有实际 I/O 事件时才工作,避免大量线程空等。

      • BIO的浪费:线程在read()被挂起,明明CPU空闲却不能干活:

      • NIO的高效

        • 线程大部分时间阻塞在select()(不消耗CPU)

        • 事件到来时批量处理多个连接(CPU集中干活)

      •  本质:把N个线程的碎片化等待,合并成1个线程的集中等待+批量处理。
    • 非阻塞操作: Channel 的非阻塞模式使得 I/O 操作不会长时间挂起线程。

      • 阻塞I/O (BIO) 的read()
        // 线程卡死在这里直到数据就绪!
        int bytesRead = inputStream.read(buffer); 
        • 若内核无数据,线程被挂起(进入休眠状态)

        • 直到数据到达,操作系统唤醒线程

      • 非阻塞I/O (NIO) 的read()
        // 立即返回,绝不卡住线程!
        int bytesRead = socketChannel.read(buffer); 
        返回值含义线程状态
        bytesRead > 0成功读到数据继续运行
        bytesRead = 0内核暂无数据,但未报错继续运行
        bytesRead = -1连接关闭继续运行
      • 操作流程:
      •  核心区别
        • 阻塞I/O:没数据时线程被操作系统强行暂停

        • 非阻塞I/O:没数据时线程立刻拿0返回继续运行
          即使数据没准备好,线程也绝不挂起!

    • 真实场景模拟(理解三者协作)

      • Selector阻塞selector.select() 暂停运行

      • Client2数据到达:操作系统唤醒Selector线程

      • Selector遍历就绪连接:发现Client2有OP_READ事件

      • 非阻塞读取

        // 尝试读取Client2数据(非阻塞调用!)
        int n = client2Channel.read(buffer);
        if(n == 0) {// 数据未就绪?不可能!因为select()已通知就绪
        } else if(n > 0) {// 处理数据
        }
        • 关键:由于select()已保证此时有数据,所以read()必然读到数据(不会返回0)

      • 处理完成后继续select():线程再次休眠等待事件

    •  设计精妙之处

      • select()阻塞是高效的(避免CPU空转)

      • Channel的非阻塞保证处理事件时线程永不挂起
        两者配合实现“等待时不耗CPU,工作时全速运行”的理想状态!

  • 缺点:

    • 编程模型复杂: 需要理解 Channel, Buffer, Selector, SelectionKey 及其交互。需要手动管理事件状态 (interestOps)。

    • API 相对底层: 需要处理粘包/拆包、连接管理、异常处理等。

    • 本质仍是同步: 虽然 Channel 是非阻塞的,但应用线程仍需主动调用 select() 轮询事件主动执行读写操作(即使数据可能没完全准备好,非阻塞调用可能返回 0 或部分数据)。真正的异步通知发生在操作系统内核到 Selector,应用线程仍需同步处理事件。

    • select() 可能成为瓶颈: 当连接数巨大且活跃连接比例很高时,遍历 SelectionKey 和处理事件可能耗时。

  • 适用场景: 需要支持高并发连接数的网络服务器(如聊天服务器、消息推送、RPC框架等)。是 Java 高性能网络编程的主流选择。 框架如 Netty, Mina 就是在 NIO 基础上构建的,封装了复杂性。

三、AIO (Asynchronous I/O - 异步 I/O) - JDK 7 (2011)

  • 模型本质: 异步非阻塞模型 (真正意义上的异步)。

  • 核心思想: 应用线程发起 I/O 操作 (如 readwriteaccept) 后立即返回无需等待操作完成。操作系统负责完成整个 I/O 操作(数据从内核空间拷贝到用户空间),完成后会主动调用应用预先注册的回调函数通知结果。

  • 核心类:

    • AsynchronousSocketChannel / AsynchronousServerSocketChannel / AsynchronousFileChannel 异步通道。

    • CompletionHandler<V, A> 定义 I/O 操作完成或失败时的回调方法 (completed(V result, A attachment)failed(Throwable exc, A attachment))。V 是操作结果类型(如读到的字节数),A 是附件类型(用于传递上下文)。

    • Future<V> 另一种处理方式,通过 Future 对象可以在之后主动 get() 结果(会阻塞)或轮询 isDone()

  • 工作原理:

    • 应用线程打开异步通道(如 AsynchronousServerSocketChannel.open())。

    • 应用线程调用异步操作:

      • accept(A attachment, CompletionHandler<AsynchronousSocketChannel, ? super A> handler)

      • read(ByteBuffer dst, A attachment, CompletionHandler<Integer, ? super A> handler)

      • write(ByteBuffer src, A attachment, CompletionHandler<Integer, ? super A> handler)

    • 调用立即返回,应用线程可以继续执行其他任务。

    • 操作系统在后台执行实际的 I/O 操作(监听连接、读取数据、写入数据)。

    • 当操作完成(成功或失败)时,操作系统会通知 Java 运行时。

    • Java 运行时(通常由内置的线程池执行)调用应用注册的 CompletionHandler 的 completed() 或 failed() 方法,传入结果或异常以及附件。

  • 伪代码示例 (服务器端 - 使用 CompletionHandler):

    ​​​​​​
    AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));// 开始异步等待客户端连接
    serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {@Overridepublic void completed(AsynchronousSocketChannel clientChannel, Void attachment) {// 1. 有新连接建立成功!立即再次调用accept等待下一个连接serverChannel.accept(null, this);// 2. 处理新连接: 异步读取客户端数据ByteBuffer buffer = ByteBuffer.allocate(1024);clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {@Overridepublic void completed(Integer bytesRead, ByteBuffer buffer) {if (bytesRead > 0) {buffer.flip();// ... 处理数据 ...// 3. 异步写响应 (伪代码略)} else if (bytesRead < 0) {// 连接关闭try { clientChannel.close(); } catch (IOException e) { ... }}// 可以继续异步读 (递归调用read)}@Overridepublic void failed(Throwable exc, ByteBuffer buffer) {// 处理读失败try { clientChannel.close(); } catch (IOException e) { ... }}});}@Overridepublic void failed(Throwable exc, Void attachment) {// 处理accept失败}
    });
    // 主线程可以继续做其他事情,或者让程序保持运行
    Thread.currentThread().join();
  • 优点:

    • 真正的异步: 应用线程发起 I/O 后完全不用管,内核完成所有工作后回调。应用线程完全解放,不参与任何等待或轮询。

    • 更高的资源利用率: 理论上线程模型更优,应用线程只负责业务逻辑和发起请求。

    • 简化某些场景: 回调机制有时比 NIO 的事件轮询更符合某些编程思维。

  • 缺点:

    • 编程模型更复杂 (回调地狱): 嵌套的回调 (CompletionHandler) 可能导致代码结构复杂、难以阅读和维护(虽然 JDK8+ 的 Lambda 有所缓解)。

    • 平台依赖性: AIO 的实现深度依赖操作系统底层的真正的异步 I/O 支持 (如 Windows 的 IOCP, Linux 的 io_uring 或 AIO,但 Linux 的 AIO 支持不完善且有限制)。在 Linux 上,JDK AIO 的实现可能基于 epoll 模拟(本质仍是同步非阻塞),性能优势不明显甚至不如 NIO。

    • 成熟度和社区支持: 相比 NIO 及其衍生框架 (Netty),AIO 的使用较少,社区最佳实践和成熟框架相对缺乏。Netty 早期尝试过 AIO 后端,但后来放弃了,主要基于 NIO。

    • 调试难度: 异步回调的堆栈跟踪可能不如同步代码直观。

  • 适用场景: 在操作系统原生 AIO 支持良好的环境(如 Windows)下,可能对某些特定 I/O 密集型任务(如大文件读写)有优势。但在主流的 Linux 服务器环境和高并发网络编程中,NIO (及其框架 Netty) 仍然是绝对的主流和推荐选择

四、三种模型对比总结

特性BIO (阻塞 I/O)NIO (非阻塞 I/O / New I/O)AIO (异步 I/O)
模型本质同步阻塞同步非阻塞 (核心) + 多路复用异步非阻塞
线程要求1 连接 ≈ 1 线程1 线程管理 N 连接 (Selector)发起线程 ≠ 完成线程 (回调线程池)
I/O 操作read()write()accept() 阻塞线程channel.read(buffer)channel.write(buffer) 非阻塞 (立即返回)read()write()accept() 立即返回 (异步)
结果获取主动等待 操作完成主动轮询/处理事件 (Selector.select())被动回调 (CompletionHandler) 或 Future 获取
复杂度简单复杂 (Channel, Buffer, Selector, 事件状态)复杂 (回调嵌套, Future)
吞吐量/并发 (受限于线程数) (单线程处理大量连接)理论最高 (依赖 OS 实现)
资源消耗 (线程多) (线程少) (线程少)
可靠性高 (成熟)高 (成熟,Netty 广泛应用)依赖 OS 实现,Linux 下可能受限
适用场景低并发,连接少且固定高并发网络应用 (主流)特定 OS 或特定 I/O 任务 (非主流)

五、实际应用建议

  • 绝对不要用原生 BIO 写新项目: 除非是极其简单的工具或测试。

  • 优先选择 NIO 框架 (强烈推荐 Netty):

    • Netty 是 Java 领域最成熟、应用最广泛的高性能异步网络框架。

    • 它基于 NIO 构建,提供了极其优雅、高效、易用的 API,封装了底层的复杂性(粘包拆包、编解码、连接管理、线程模型等)。

    • 广泛应用于 RPC (Dubbo, gRPC-Java)、消息队列 (RocketMQ)、游戏服务器、HTTP/2 服务器 (如 Armeria)、分布式协调(Zookeeper客户端) 等几乎所有需要高性能网络通信的 Java 项目中。

  • 谨慎对待 AIO:

    • 了解其概念,知道它是真正的异步模型。

    • 但在生产环境,尤其是 Linux 服务器上,优先使用 Netty (NIO)。除非有非常明确的证据表明在特定 Windows 场景下 AIO 有显著优势且能满足需求。

  • 理解底层原理: 即使使用 Netty,理解 BIO/NIO/AIO 的底层原理、同步/异步/阻塞/非阻塞的区别,对于设计高性能系统、排查问题至关重要。

http://www.lryc.cn/news/588099.html

相关文章:

  • Python Web框架对比:Flask vs FastAPI
  • Python数据容器-字典dict
  • 丑团-h5-Mtgsig算法-分析
  • Linux基础开发工具(3)
  • ACL流量控制实验
  • 车载诊断框架 --- 车载诊断GuideLine
  • 信息收集的骚打法
  • 安装llama-factory报错 error: subprocess-exited-with-error
  • SQL创建三个表
  • 国产LVDT信号调理芯片XJD698对比AD698的技术突破与优势解析
  • 代码随想录算法训练营第三十五天|416. 分割等和子集
  • CLIP、Open CLIP、SigLip、SigLip2的相关总结
  • 内网环境自签名超长期HTTPS证书,并在Chrome中显示为安全证书
  • Faiss能解决什么问题?Faiss是什么?
  • 【数据结构初阶】--单链表(二)
  • Kafka Broker源码解析(上篇):存储引擎与网络层设计
  • 【html基本界面】
  • [spring6: ResolvableType TypeDescriptor ConversionService]-类型系统
  • [笔记] 动态 SQL 查询技术解析:构建灵活高效的企业级数据访问层
  • 电力协议处理框架C++版(三)
  • 打破空间边界!Nas-Cab用模块化设计重构个人存储逻辑
  • SwiftUI 全面介绍与使用指南
  • AI数字人正成为医药行业“全场景智能角色”,魔珐科技出席第24届全国医药工业信息年会
  • 【微信小程序】
  • 1.2.2 高级特性详解——AI教你学Django
  • vue3 服务端渲染时请求接口没有等到数据,但是客户端渲染是请求接口又可以得到数据
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘sqlite3’问题
  • 第一章编辑器开发基础第一节绘制编辑器元素_4输入字段(4/7)
  • Django基础(一)———创建与启动
  • Django Admin 配置详解