杰瑞科技汇

java aio nio bio

核心概念:I/O 模型

在深入代码之前,我们先理解几个核心概念:

java aio nio bio-图1
(图片来源网络,侵删)
  • 阻塞:当一个线程发起一个 I/O 操作(如读取数据),如果数据还没准备好,这个线程就会一直等待,直到数据准备好或发生超时,在此期间,线程被“阻塞”,无法执行其他任何任务。
  • 非阻塞:当一个线程发起一个 I/O 操作,如果数据还没准备好,它会立即得到一个“未就绪”的返回,然后线程可以立即去做其他事情,过一段时间再来检查数据是否准备好。
  • 同步:由用户线程自己去执行 I/O 操作(比如通过循环调用 read 方法来检查数据是否就绪),数据在用户线程和内核之间进行拷贝。
  • 异步:用户线程发起 I/O 操作后,就可以去做其他事情了,当 I/O 操作完成(数据已准备好并拷贝到用户空间)后,操作系统会通知用户线程(通过回调或事件),整个过程由操作系统内核完成,用户线程无需主动干预。

基于这些概念,我们可以组合出四种 I/O 模型:

  1. 同步阻塞 I/O (BIO)
  2. 同步非阻塞 I/O (NIO 的一种实现方式)
  3. I/O 多路复用 (NIO 的核心)
  4. 异步 I/O (AIO)

BIO (Blocking I/O) - 阻塞式 I/O

BIO 是 Java 最早提供的 I/O 模型,也是最简单、最容易理解的模型。

工作原理

  1. 服务端:启动一个 ServerSocket,在一个固定端口进行监听。
  2. 客户端:发起连接请求。
  3. 服务端处理
    • 服务端通过 ServerSocket.accept() 方法来等待客户端连接。
    • accept() 是一个阻塞方法,如果没有客户端连接,服务端线程会一直阻塞无法处理其他任何连接。
    • 当一个客户端连接成功,accept() 返回一个 Socket 对象,代表与这个客户端的连接通道。
  4. 数据读写
    • 服务端会为这个新的 Socket 创建一个新的线程,在这个线程中进行数据的读写操作(InputStream.read() / OutputStream.write())。
    • read()write() 方法也都是阻塞方法,如果客户端没有发送数据,读线程会一直阻塞;如果发送缓冲区满了,写线程也会阻塞。
  5. 线程销毁:当通信结束,这个处理线程随之销毁。

代码示例 (BIO)

// 服务端
public class BioServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("Server started, listening on port 8080...");
        while (true) {
            // 1. 阻塞等待客户端连接
            Socket clientSocket = serverSocket.accept();
            System.out.println("Client connected: " + clientSocket.getInetAddress().getHostAddress());
            // 2. 为每个客户端创建一个新线程处理
            new Thread(() -> {
                try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                     PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
                    String inputLine;
                    // 3. 阻塞读取客户端数据
                    while ((inputLine = in.readLine()) != null) {
                        System.out.println("Received from client: " + inputLine);
                        out.println("Server response: " + inputLine);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        clientSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

优缺点

  • 优点

    模型简单,编码直观,易于理解。

  • 缺点
    • 性能差:每个连接都需要一个独立的线程,如果连接数非常多(比如成千上万),线程数会急剧膨胀,导致服务器资源(内存、CPU)耗尽,发生线程切换开销巨大等问题。
    • 可靠性差:一个线程的阻塞(如长时间不发送数据)会影响整个服务,甚至可能导致宕机。
    • 可扩展性差:无法应对高并发场景。

适用场景

  • 连接数较少且固定的场景,如一些简单的内部应用、教学示例。
  • 对性能要求不高的传统应用。

NIO (New I/O) - 非阻塞 I/O / I/O 多路复用

为了解决 BIO 的高并发问题,Java 1.4 引入了 NIO,NIO 的核心思想是用一个线程来管理多个连接,通过轮询的方式检查哪些连接已经准备好可以进行 I/O 操作。

java aio nio bio-图2
(图片来源网络,侵删)

核心组件

NIO 主要由三个核心部分组成:

  1. Channel (通道):类似传统的 Stream,但双向的,既可以读也可以写。SocketChannelServerSocketChannel 是网络编程中最重要的两种通道。
  2. Buffer (缓冲区):数据被读取到一个 Buffer 中,或者从 Buffer 中写入。Buffer 是一块连续的内存区域,读写操作都是通过 Buffer 进行的,NIO 的读写都是缓冲区操作,而不是流操作。
  3. Selector (选择器):NIO 的“魔法”所在,一个 Selector 可以同时监控多个 Channel 的 I/O 状态(如连接、读、写),当某个 Channel 有“就绪”的 I/O 事件时,Selector 会将其“选择”出来,然后由我们进行处理。

工作原理

  1. 服务端:创建一个 ServerSocketChannel,并将其设置为非阻塞模式
  2. 绑定 Selector:将这个 ServerSocketChannel 注册到 Selector 上,并指定我们关心的事件(SelectionKey.OP_ACCEPT,即“接受连接”)。
  3. 轮询:在一个单独的线程中,启动一个无限循环,调用 Selector.select() 方法。
    • select() 也是一个阻塞方法,但它不是阻塞在某个连接上,而是阻塞在 Selector,它会一直等待,直到至少有一个注册的 Channel 发生了我们关心的事件。
  4. 处理就绪事件:当 select() 返回后,我们通过 Selector.selectedKeys() 获取所有“就绪”的 Channel 集合。
  5. 分发处理:遍历这个集合,根据事件的类型(OP_ACCEPT, OP_READ, OP_WRITE)进行相应的处理:
    • 如果是 OP_ACCEPT,说明有新的客户端连接,接受连接,并得到一个 SocketChannel
    • 将这个新的 SocketChannel 也设置为非阻塞模式,并注册到 Selector 上,这次我们关心的是 OP_READ 事件。
    • 如果是 OP_READ,说明某个 SocketChannel 有数据可读,创建一个 Buffer,从 Channel 中读取数据到 Buffer 中。

代码示例 (NIO)

// 服务端
public class NioServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建Selector和ServerSocketChannel
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
        // 2. 将ServerSocketChannel注册到Selector,关心OP_ACCEPT事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO Server started, listening on port 8080...");
        while (true) {
            // 3. 阻塞等待至少一个通道就绪
            selector.select();
            // 4. 获取所有就绪的SelectionKey
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                keyIterator.remove(); // 手动移除,防止重复处理
                if (key.isAcceptable()) {
                    // 5. 处理连接就绪事件
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = ssc.accept();
                    socketChannel.configureBlocking(false);
                    // 将新的SocketChannel注册到Selector,关心OP_READ事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("Client connected: " + socketChannel.getRemoteAddress());
                }
                if (key.isReadable()) {
                    // 6. 处理读就绪事件
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = socketChannel.read(buffer);
                    if (bytesRead == -1) {
                        // 客户端关闭连接
                        key.cancel();
                        socketChannel.close();
                        System.out.println("Client disconnected.");
                        continue;
                    }
                    buffer.flip();
                    System.out.println("Received from client: " + new String(buffer.array(), 0, bytesRead));
                    // 简单回写
                    String response = "Server response: " + new String(buffer.array(), 0, bytesRead);
                    socketChannel.write(ByteBuffer.wrap(response.getBytes()));
                }
            }
        }
    }
}

优缺点

  • 优点
    • 高并发:一个线程可以管理成百上千个连接,极大地减少了线程数和上下文切换的开销。
    • 可扩展性好:非常适合处理高并发连接。
  • 缺点
    • 编程复杂:需要理解 Channel, Buffer, Selector 等概念,代码比 BIO 复杂。
    • “伪”异步:虽然一个线程可以处理多个连接,但数据的读写仍然是同步的,当一个 Channel 的数据读写时间很长时,会影响到其他 Channel 的处理(因为是在同一个线程的循环中)。

适用场景

  • 高并发服务器,如 Web 服务器、RPC 框架、聊天室等。
  • 需要处理大量连接,但每个连接的 I/O 操作相对较短的场景。

AIO (Asynchronous I/O) - 异步 I/O

AIO 是 Java 7 中引入的 I/O 模型,是真正的“异步非阻塞” I/O,它将 I/O 操作完全交由操作系统内核完成,应用程序无需主动轮询,只需在 I/O 操作完成后通过回调函数或 Future 对象获取结果。

工作原理

  1. 服务端:创建一个 AsynchronousServerSocketChannel,并绑定端口。
  2. 发起异步操作:调用 AsynchronousServerSocketChannel.accept() 方法,并传入一个 CompletionHandler(回调处理器)。
    • accept() 方法是非阻塞的,它会立即返回,主线程可以继续执行其他任务。
  3. 内核处理:操作系统在后台(由专门的 I/O 线程池)等待并处理客户端的连接请求。
  4. 回调通知:当客户端连接成功,操作系统会自动调用我们之前传入的 CompletionHandlercompleted 方法,并将代表连接的 AsynchronousSocketChannel 对象作为参数传递过来。
  5. 读写操作:在 completed 方法中,我们同样可以发起异步的读写操作(如 read(ByteBuffer buffer, CompletionHandler)),同样传入回调处理器,形成一个异步链式调用。

代码示例 (AIO)

// 服务端
public class AioServer {
    public static void main(String[] args) throws IOException {
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));
        System.out.println("AIO Server started, listening on port 8080...");
        // 1. 开始接受连接,传入一个CompletionHandler
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
                // 2. 如果成功,继续接受下一个连接(递归调用)
                serverChannel.accept(null, this);
                System.out.println("Client connected: " + clientChannel.getRemoteAddress());
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                // 3. 异步读取数据
                clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer bytesRead, ByteBuffer attachment) {
                        if (bytesRead == -1) {
                            try {
                                clientChannel.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                            return;
                        }
                        attachment.flip();
                        System.out.println("Received from client: " + new String(attachment.array(), 0, bytesRead));
                        String response = "Server response: " + new String(attachment.array(), 0, bytesRead);
                        // 4. 异步写回数据
                        clientChannel.write(ByteBuffer.wrap(response.getBytes()), null, new CompletionHandler<Integer, Void>() {
                            @Override
                            public void completed(Integer result, Void attachment) {
                                // 写完成,可以继续读或其他操作
                                // 这里为了简化,我们不继续读了
                            }
                            @Override
                            public void failed(Throwable exc, Void attachment) {
                                exc.printStackTrace();
                            }
                        });
                    }
                    @Override
                    public void failed(Throwable exc, ByteBuffer attachment) {
                        exc.printStackTrace();
                    }
                });
            }
            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
            }
        });
        // 主线程不能退出,否则程序会终止
        // 在真实应用中,可能需要一个while(true)来保持主线程运行
        // 或者使用CountDownLatch等工具
        System.out.println("Server is running...");
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

优缺点

  • 优点
    • 真正的异步:性能最高,I/O 操作完全由内核和 I/O 线程处理,业务线程(主线程)完全不被阻塞。
    • 编程模型更符合“异步”思维:通过回调或 Future 处理结果,逻辑清晰。
  • 缺点
    • 编程复杂:回调地狱(Callback Hell),代码逻辑难以追踪和维护。
    • 系统开销大:依赖操作系统的 AIO 实现,在 Windows 上表现良好,但在 Linux 上,其性能提升并不总是明显,且可能比 NIO 更消耗资源。
    • 生态不完善:相比 NIO,AIO 的使用案例和社区支持较少。

适用场景

  • 超高并发、I/O 密集型的应用,如网络存储、高性能消息中间件。
  • 对延迟要求极高的场景。

总结与对比

特性 BIO (Blocking I/O) NIO (New I/O) AIO (Asynchronous I/O)
I/O 模型 同步阻塞 同步非阻塞 / I/O 多路复用 异步非阻塞
核心思想 一个连接一个线程 一个线程管理多个连接 I/O 操作由内核完成,线程回调
线程模型 每个连接一个新线程 单线程或少量线程管理所有连接 业务线程与 I/O 线程分离
阻塞点 accept(), read(), write() select() (阻塞在 Selector 上) 无阻塞,通过回调通知
性能 低,无法高并发 高,可处理大量连接 极高,但依赖系统和实现
编程复杂度 简单 复杂 复杂,易出现回调地狱
JDK 版本 0+ 4+ 7+
适用场景 低并发、连接数少 高并发、通用场景 超高并发、延迟敏感型

如何选择?

  • 新手或简单应用:从 BIO 开始,简单直观。
  • 绝大多数高并发应用NIO 是目前 Java 网络编程的首选和事实标准,像 Netty、Vert.x 等优秀的网络框架都是基于 NIO 实现的,它在性能、复杂度和适用性之间取得了最好的平衡。
  • 极限性能挑战:如果你的应用是顶级的 I/O 密集型服务,并且你对 AIO 的复杂性有驾驭能力,可以尝试 AIO,但通常情况下,一个精心设计的 NIO 方案已经足够强大。
java aio nio bio-图3
(图片来源网络,侵删)
分享:
扫描分享到社交APP
上一篇
下一篇