BIO、NIO、AIO 的区别

140 阅读5分钟

1、BIO(Blocking I/O,同步阻塞I/O)

  • 定义:传统的同步阻塞模型,线程在读写数据时会一直阻塞,直到操作完成。

  • 工作原理

    • 每个客户端连接需要一个独立的线程处理。
    • 线程在 read() 或 write() 时会被阻塞,无法执行其他任务。
  • 特点

    • 简单易用:代码逻辑直观。
    • 高资源消耗:每个连接占用一个线程,并发量高时线程数激增,易导致性能瓶颈。
  • 适用场景

    • 低并发、连接数较少的应用(如简单文件读写)。
  • 示例

    ServerSocket serverSocket = new ServerSocket(8080);
    while (true) {
        Socket socket = serverSocket.accept(); // 阻塞等待连接
        new Thread(() -> {
            // 处理读写操作
        }).start();
    }
    

    对于以上代码,我想到了下面的可视化解释:
    想象一个公司前台,服务器就像是一个门,门的编号是 8080。
    这个门会一直等待有人敲门(客户端连接)。

    • ServerSocket serverSocket = new ServerSocket(8080);
      这是创建一个门(服务器),门的编号是 8080。门会一直开着,等待有人敲门。
    • while (true)
      这是一个无限循环,表示门会一直开着,不会关闭。
    • Socket socket = serverSocket.accept();
      这是门在等待有人敲门。当有人敲门时,门会接受这个请求,并生成一个“接待员”(Socket 对象)来处理这个客户。
    • new Thread(() -> { ... }).start();
      每当有客户敲门,服务器会分配一个接待员(线程)来专门处理这个客户的需求。这样,即使门还在等待其他客户,接待员可以独立地处理当前客户的请求。

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

  • 定义:基于事件驱动的多路复用模型,线程不会被单个I/O操作阻塞。

  • 核心组件

    • Channel:替代传统流(Stream),支持双向读写(如 SocketChannelServerSocketChannel)。
    • Buffer:数据读写的中转缓冲区(如 ByteBuffer)。
    • Selector:多路复用器,监听多个通道的事件(如连接、读、写)。
  • 工作原理

    • 线程通过 Selector 轮询注册的 Channel,仅处理已就绪的事件。
    • 非阻塞模式下,read() 和 write() 立即返回,若数据未就绪,线程可执行其他任务。
  • 特点

    • 高并发支持:单线程可管理多个连接,减少线程资源消耗。
    • 复杂实现:需要处理事件循环、缓冲区管理等。
  • 适用场景

    • 高并发、短连接场景(如聊天服务器、即时通讯)。
  • 示例

    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> keys = selector.selectedKeys();
        for (SelectionKey key : keys) {
            if (key.isAcceptable()) {
                // 处理新连接
            } else if (key.isReadable()) {
                // 处理读事件
            }
        }
    }
    

    同样的,对于这段代码给出以下的解释:

    想象一个大楼的门禁系统:

  • Selector 是一个警报器,它可以同时监控多个门(Channel)。

  • ServerSocketChannel 是大楼的主门,门的编号是 8080。

  • SelectionKey 是警报器上的一个指示灯,表示某个门的状态(比如有人敲门或门铃响了)。

  • selector.select() 是警报器在等待,直到有门的状态发生变化(比如有人敲门或门铃响了)。

  • SelectionKey 是警报器上的一个指示灯,表示某个门的状态:

    • isAcceptable() :表示有人敲门(新连接请求)。
    • isReadable() :表示门铃响了(数据可读)。
    Selector selector = Selector.open(); // 创建一个警报器
    ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 创建主门
    serverChannel.bind(new InetSocketAddress(8080)); // 主门的编号是 8080
    serverChannel.configureBlocking(false); // 主门设置为非阻塞模式
    serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 注册警报器,监听“有人敲门”事件
    
    while (true) { // 无限循环,保持警报器工作
        selector.select(); // 警报器阻塞等待事件发生
        Set<SelectionKey> keys = selector.selectedKeys(); // 获取所有触发的事件
        for (SelectionKey key : keys) {
            if (key.isAcceptable()) { // 如果是“有人敲门”事件
                // 处理新连接(比如接受连接并注册到警报器)
            } else if (key.isReadable()) { // 如果是“门铃响了”事件
                // 处理读操作(比如读取客户端发送的数据)
            }
        }
    }
    

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

  • 定义:基于事件回调的异步模型,I/O操作完成后系统主动通知线程。

  • 工作原理

    • 发起 read() 或 write() 后,线程继续执行其他任务。
    • 操作系统完成I/O操作后,通过回调(如 CompletionHandler)通知结果。
  • 特点

    • 真正的异步:无需轮询,资源利用率高。
    • 编程复杂度高:回调逻辑需妥善处理,避免嵌套地狱。
  • 适用场景

    • 高吞吐量、长连接场景(如文件服务器、大规模数据传输)。
  • 示例

    AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
    server.bind(new InetSocketAddress(8080));
    
    server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
        @Override
        public void completed(AsynchronousSocketChannel client, Void attachment) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer buffer) {
                    // 处理读取的数据
                }
                @Override
                public void failed(Throwable exc, ByteBuffer buffer) {}
            });
        }
        @Override
        public void failed(Throwable exc, Void attachment) {}
    });
    

    同样的,我想到了一个比较完美的解释
    想象一个快递站(服务器),它等待快递员(客户端)来送包裹(数据)。快递站不需要一直盯着门口,而是当有快递员来时,会自动处理包裹。

AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(); // 创建快递站
server.bind(new InetSocketAddress(8080)); // 快递站的地址是 8080

server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { // 快递站开始等待快递员
    @Override
    public void completed(AsynchronousSocketChannel client, Void attachment) { // 当快递员到达时
        ByteBuffer buffer = ByteBuffer.allocate(1024); // 准备一个包裹(缓冲区)
        client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() { // 快递员开始处理包裹
            @Override
            public void completed(Integer result, ByteBuffer buffer) { // 包裹处理完成
                // 处理读取的数据(比如解析数据或发送响应)
            }
            @Override
            public void failed(Throwable exc, ByteBuffer buffer) { // 包裹处理失败
                // 处理错误
            }
        });
    }
    @Override
    public void failed(Throwable exc, Void attachment) { // 快递员未到达(连接失败)
        // 处理连接失败
    }
});