2024-04-04
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/5829408636

回答

BIO 阻塞式 I/O,NIO 同步非阻塞 I/O,AIO 异步非阻塞 I/O。它们三者之间有如下几个区别。

  • I/O 模型
    • BIO:同步且阻塞 I/O 模型。服务端模型为一个连接一个线程。
    • NIO:同步非阻塞 I/O 模型。服务端模型为一个线程处理多个连接。
    • AIO:异步非阻塞 I/O 模型。服务端模型为一个有效请求一个线程。
  • 阻塞与否
    • BIO:阻塞,即在读写操作完成之前,线程会一直等待,直到操作完成。
    • NIO:非阻塞,即线程在请求读写操作时会立即返回,可以进行其他任务。
    • AIO:非阻塞且异步,当操作完成时,会被动地通知或回调线程进行后续操作。
  • 线程模型
    • BIO:一个连接一个线程,对于每个新的客户端连接都需要创建一个新的线程。
    • NIO:多路复用,使用单个或几个线程来管理多个连接,通过 Selector 监控客户端的连接和请求。
    • AIO:异步任务完成后,系统会主动回调,使用线程池来处理这些回调,从而实现高效的并发处理。
  • 应用场景
    • BIO:适用于连接数比较少的场景,实现较为简单,入门级。
    • NIO:适用于连接数较多且连接较短的场景(例如聊天),实现稍微复杂,需要理解 Buffer、Channel、Selector 等 Java NIO 的概念。
    • AIO:适合于高负载、高并发的场景,但是实现比较复杂,需要理解异步编程模型。

详解

BIO

BIO(Blocking I/O),即阻塞式IO,是传统的Java I/O编程模型。在BIO,当一个线程调用读写数据的方法时,该线程会被阻塞,直到有一些数据被读取或数据完全被写入。这期间,该线程无法进行其他任务,只有当数据完全传输完成后,线程才能继续执行。

BIO 的服务端模型为一个连接一个线程。在 BIO 模式中,服务端通常由一个主线程负责监听客户端的连接请求,每当接收到一个客户端连接请求时,主线程都会为这个请求创建一个新的线程来进行处理。图例如下:

示例如下:

public class BIOServer {
    public static void main(String[] args) {
        // 创建服务器端的ServerSocket,监听8080端口
        ServerSocket serverSocket = new ServerSocket(8080);
        while (true) {
            // 等待客户端连接,此处会阻塞
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端连接成功:" + clientSocket.getInetAddress().getHostAddress());

            // 为每个客户端创建一个新的线程进行处理
            new Thread(() -> {
                try {
                    // 读取客户端发送的数据
                    BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                    String clientData = reader.readLine();
                    System.out.println("接收到客户端的数据:" + clientData);

                    // 处理业务
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

由于该模式每个连接都需要新建一个线程,当连接较多时,就会创建大量线程,严重消耗系统资源,限制了服务的扩展能力,所以它只适用于连接较少的应用场景。同时,由于它是同步阻塞的,所以整体性能不是很高。它的优点嘛,就是模型简单,实现起来没有什么困难之处。

NIO

NIO,即同步非阻塞 IO。NIO 主要解决了 BIO 因阻塞而导致的效率低下问题。在 NIO 模型中,一个服务端线程可以管理多个连接。它的服务端模型为一个线程处理多个连接,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理,图例如下:

NIO 引入 ChannelBufferSelector 三个核心概念,实现了数据的异步处理和通道的多路复用,大大提高了IO操作的效率,表现了超高的性能和扩展性。

  • Channel:通道,可以看作是连接数据源的开放链接,如文件、网络套接字等等。与传统 IO 的流不同, Channel 是双向的,既可以用来进行读操作,也可以用来进行写操作。
  • Buffer:字节缓冲区,它是数据的容器。
  • Selector:选择器。Selector 是 NIO 实现单独线程管理多个链接的关键机制,它会不断轮询注册到其上的 Channel,当某个 Channel 上的 IO 操作准备就绪时,能够高效地进行处理。

NIO 使用单一线程来管理多个连接,采用多路复用机制,使用较少的线程来处理连接,减少了线程资源的消耗,同时也减少了上下文的切换,提高了系统的处理能力,适用于连接数多且连接比较短(轻操作)的架构,但是它的实现稍微会复杂写。

由于 NIO 的高性能和高可扩展性,使得它是目前开发高性能网络服务器使用最为广泛的模型。

下面是一个简单的代码:

public class NIOServer {
    public static void main(String[] args) throws Exception {
        // 打开服务器套接字通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 绑定监听端口
        serverSocketChannel.bind(new InetSocketAddress(8080));
        // 打开选择器
        Selector selector = Selector.open();
        // 将服务器套接字通道注册到选择器上,设置为接受连接操作
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("服务器启动,监听端口:8080");

        while (true) {
            // 选择一组键,其相应的通道已为I/O操作准备就绪
            selector.select();
            // 获取选择键集
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 处理接受连接就绪事件
                if (key.isAcceptable()) {
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = serverChannel.accept();
                    socketChannel.configureBlocking(false);
                    // 将新连接的通道注册到选择器上,设置为读就绪操作
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                }
                // 处理读就绪事件
                if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int length = socketChannel.read(buffer);
                    if (length > 0) {
                        buffer.flip();
                        System.out.println("接收到客户端的数据:" + new String(buffer.array(), 0, length));
                        buffer.clear();
                    }
                }
                iterator.remove();
            }
        }
    }
}

AIO

AIO,即异步非阻塞 IO,AIO 提供了一种完全异步的概念,在该模型下,IO 操作是完全异步的,即我们执行一个 IO 操作后我们不需要等待它完成,可以直接进入其他任务,当 IO 操作完成时,它会触发一个回调函数来处理结果。理解该模型我们需要理解如下几个概念。

  • **异步通道:**Java 提供了 AsynchronousSocketChannelAsynchronousServerSocketChannel 异步通道,这两个 Channel 提供了对异步非阻塞 IO 的支持,它允许我们进行异步读写操作,当操作完成时,可以接收到通知。
  • CompletionHandler:CompletionHandler接口是AIO中处理异步操作结果的回调接口。它包含了completed()failed() 两个方法,分别在操作成功完成和失败时被调用。

AIO 的服务端模型为一个有效请求一个线程,其工作原理如下:

  1. 执行一个异步 IO 操作后,Java 程序可以立刻返回去执行其他任务。
  2. 实际的 AIO 操作由操作系统异步完成。
  3. 当异步 IO 操作完成后,会回调相应的 CompletionHandler 回调函数。

下面是一个简单的 AIO 服务端示例代码:

public class SimpleAIOServer {
    public static void main(String[] args) throws Exception {
        // 打开异步服务器套接字通道
        final AsynchronousServerSocketChannel listener =
                AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));

        // 接受客户端连接(异步)
        listener.accept(null, new CompletionHandler<AsynchronousSocketChannel,Void>() {
            public void completed(AsynchronousSocketChannel ch, Void att) {
                // 接受下一个连接
                listener.accept(null, this);

                // 处理当前连接
                handleClient(ch);
            }

            public void failed(Throwable exc, Void att) {
                // 处理错误
            }
        });
    }

    static void handleClient(AsynchronousSocketChannel ch) {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 异步读取数据
        ch.read(buffer, null, new CompletionHandler<Integer,Void>() {
            public void completed(Integer result, Void attachment) {
                // 处理
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                // 处理错误
            }
        });
    }  
}

AIO 模型非常适合高负载、高并发的场景。但是它的实现会比较复杂。


更多阅读

你知道几种 I/O 模型?

阅读全文