1. BIO (Blocking I/O - 同步阻塞 I/O)

  • 核心思想: 应用程序发起 I/O 操作(如读取数据)后,线程会被阻塞,直到数据准备好(从内核空间复制到用户空间)或操作完成。在此期间,该线程不能执行其他任务。
  • 工作方式:
    • 线程调用 read() 方法。
    • 线程等待,直到内核将数据准备好(例如,网络数据包到达网卡并被内核接收)。
    • 内核将数据从内核缓冲区复制到用户空间(应用程序的内存)。
    • read() 返回,线程解除阻塞,继续执行。
  • 特点:
    • 简单易用: 编程模型直观,易于理解。
    • 资源消耗大: 每个连接通常需要一个独立的线程处理。当连接数很多时,需要大量线程,导致:
      • 线程上下文切换开销巨大。
      • 内存占用高(每个线程都需要栈空间)。
    • 效率低: 线程在等待 I/O 时处于空闲状态,CPU 资源浪费严重。
  • 典型应用场景:
    • 连接数较少且相对固定的应用(如早期的服务器)。
    • 对并发要求不高的内部系统。
  • 代码示例 (伪代码):
    while (true) {
        Socket clientSocket = serverSocket.accept(); // 阻塞等待新连接
        new Thread(() -> {
            InputStream in = clientSocket.getInputStream();
            while (true) {
                byte[] buffer = new byte[1024];
                int len = in.read(buffer); // 阻塞等待数据读取
                if (len == -1) break;
                // 处理数据...
            }
        }).start();
    }
    

2. NIO (Non-blocking I/O / New I/O - 同步非阻塞 I/O)

  • 核心思想: 应用程序发起 I/O 操作后,线程不会被阻塞。线程会立即返回一个状态(成功、失败或“未就绪”)。线程需要轮询或通过事件通知机制来检查 I/O 操作是否就绪。当就绪后,实际的 I/O 操作(数据复制)本身仍然是同步的、需要线程来完成的。
  • 工作方式 (关键组件):
    • Channel: 代表一个连接(如 SocketChannel, ServerSocketChannel, FileChannel)。可以配置为非阻塞模式。
    • Buffer: 用于数据的读写。数据在 Channel 和 Buffer 之间传输。
    • Selector: 核心组件。一个线程可以管理多个 Channel。Selector 会监控注册在其上的多个 Channel 的 I/O 事件(如连接就绪、读就绪、写就绪)。
    • SelectionKey: 代表一个 Channel 在 Selector 上的注册关系,包含了 Channel 和感兴趣的事件集合。
  • 工作流程:
    • 将 Channel 注册到 Selector,并指定感兴趣的事件(如 OP_ACCEPT, OP_READ, OP_WRITE)。
    • 线程调用 Selector.select() 方法。此方法会阻塞(可设置超时),直到至少有一个 Channel 有事件就绪。
    • 当有事件发生时,select() 返回,线程获取到就绪的 SelectionKey 集合。
    • 线程遍历 SelectionKey 集合,根据事件类型(如 isAcceptable(), isReadable())进行相应的处理(如接受连接、读取数据、写入数据)。注意:处理数据(读/写)的过程仍然是同步的。
  • 特点:
    • 高并发: 一个线程(或少量线程)可以处理大量连接,大大减少了线程数量和上下文切换开销。
    • 非阻塞: 线程不会被长时间阻塞在单个 I/O 操作上,提高了 CPU 利用率。
    • 复杂度高: 编程模型比 BIO 复杂得多,需要理解 Channel、Buffer、Selector 的交互。
    • 空轮询: 在某些系统(如 Linux)上,Selector.select() 可能在没有事件时意外返回(称为空轮询),需要额外处理。
  • 典型应用场景:
    • 高并发服务器应用(如聊天服务器、游戏服务器)。
    • 需要处理大量连接的场景。
  • 代码示例 (伪代码):
    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(); // 阻塞等待事件
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        Iterator<SelectionKey> iter = selectedKeys.iterator();
        while (iter.hasNext()) {
            SelectionKey key = iter.next();
            iter.remove();
            if (key.isAcceptable()) {
                // 处理新连接
                SocketChannel clientChannel = serverChannel.accept();
                clientChannel.configureBlocking(false);
                clientChannel.register(selector, SelectionKey.OP_READ); // 注册监听读事件
            } else if (key.isReadable()) {
                // 处理读事件
                SocketChannel clientChannel = (SocketChannel) key.channel();
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int len = clientChannel.read(buffer); // 非阻塞读取,可能读0字节
                if (len > 0) {
                    buffer.flip();
                    // 处理读取到的数据...
                } else if (len == -1) {
                    // 连接关闭
                    key.cancel();
                    clientChannel.close();
                }
            } // ... 处理写事件等
        }
    }
    

3. AIO (Asynchronous I/O - 异步非阻塞 I/O)

  • 核心思想: 应用程序发起 I/O 操作后,立即返回,不需要等待数据就绪。当内核完成整个 I/O 操作(包括数据准备和数据从内核空间复制到用户空间)后,会主动通知应用程序(通常通过回调函数)。应用程序的线程在整个过程中完全不会被阻塞。
  • 工作方式:
    • 应用程序调用一个异步 I/O 操作(如 AsynchronousSocketChannel.read())。
    • 操作系统内核负责执行 I/O 操作(等待数据到达、将数据复制到用户空间缓冲区)。
    • 应用程序线程立即返回,可以继续执行其他任务。
    • 当内核完成整个 I/O 操作后,会触发一个回调函数(或通知机制,如 Future),应用程序在该回调函数中处理完成的结果(数据或状态)。
  • 特点:
    • 真正的异步: 应用程序线程在整个 I/O 过程中完全不被阻塞。内核负责完成所有工作并通知结果。
    • 高效: 线程资源利用率最高,可以将计算资源用于其他任务。
    • 编程模型复杂: 需要处理回调函数或 Future,代码结构可能变得分散(回调地狱),需要良好的设计模式(如 Promise, async/await)。
    • 实现差异: 不同操作系统对 AIO 的支持程度和实现方式不同(如 Linux 的 AIO 支持在早期并不完善)。
  • 典型应用场景:
    • 需要极高并发和吞吐量,且对延迟有严格要求的应用。
    • 文件 I/O 操作(在某些平台上 AIO 对文件操作支持较好)。
    • 数据库连接池等。
  • 代码示例 (伪代码 - CompletionHandler 方式):
    AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
    serverChannel.bind(new InetSocketAddress(8080));
    
    // 异步接受连接
    serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
        @Override
        public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
            // 连接接受成功,继续接受下一个连接
            serverChannel.accept(null, this);
    
            // 为新的客户端通道设置读操作
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 异步读取数据
            clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer bytesRead, ByteBuffer buffer) {
                    if (bytesRead > 0) {
                        buffer.flip();
                        // 处理读取到的数据...
                        buffer.clear();
                        // 继续下一次异步读取
                        clientChannel.read(buffer, buffer, this);
                    } else if (bytesRead == -1) {
                        // 连接关闭
                        try { clientChannel.close(); } catch (IOException e) {}
                    }
                }
                @Override
                public void failed(Throwable exc, ByteBuffer buffer) {
                    // 处理读取失败
                }
            });
        }
        @Override
        public void failed(Throwable exc, Void attachment) {
            // 处理接受连接失败
        }
    });
    

总结与区别

特性 BIO (同步阻塞) NIO (同步非阻塞) AIO (异步非阻塞)
阻塞点 accept(), read(), write() 仅在 Selector.select() (可设置超时) 完全不阻塞
模式 同步 同步 异步
线程模型 一连接一线程 一请求一线程 / Reactor 模型 (单/多 Reactor) Proactor 模型
线程数量 多 (与连接数相关) 少 (通常与 CPU 核数相关)
复杂度 最高
吞吐量 最高
适用场景 连接数少、简单应用 高并发、长连接 超高并发、短连接、对延迟要求极高
核心 线程阻塞等待 Selector 轮询事件就绪 内核完成操作后回调通知
数据拷贝 线程同步等待并拷贝 事件就绪后线程同步拷贝 内核异步完成拷贝后通知

简单理解:

  • BIO: 你去餐厅点餐,点完后就站在柜台前等厨师做好,期间不能做别的事(线程阻塞)。
  • NIO: 你去餐厅点餐,点完后拿到一个号牌。你可以去逛商场(线程可以做其他事),但需要时不时回来看大屏幕(Selector.select())是否叫到你的号(事件就绪)。叫到号后,你需要自己去柜台取餐(线程同步拷贝数据)。
  • AIO: 你去餐厅点餐,点完后拿到一个号牌。你可以自由活动(线程自由)。餐厅服务员(操作系统内核)会负责把餐做好并送到你指定的位置(用户缓冲区),然后打电话通知你(回调)餐已经送到了,你可以直接享用(处理数据)。你完全不需要关心厨师做饭和送餐的过程。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐