Java中IO介绍(BIO | NIO | AIO)

159 阅读7分钟

IO是什么

IO(Input/Output),即输入/输出,是指程序与外部设备之间进行数据交换的过程。在Java中,IO操作是非常重要的部分,如文件的读写、网络通信等。

Java中IO有哪些分类

Java中的IO类型可以按照不同的工作方式分为三种,即:BIO、NIO和AIO。

1. BIO

BIO(Blocking IO)即阻塞式IO,是Java中最基础的IO模型。在BIO模型中,线程在执行IO操作时会被阻塞,直到该操作完成。这种模型简单直观,但效率较低,特别是在处理大量并发请求时,容易导致线程资源浪费和系统性能下降。

特点:

  • 简单易用:IO操作是同步执行的,可以按照传统的顺序编程方式来处理IO。
  • 线程阻塞:IO操作期间,线程被阻塞,无法执行其他任务。
  • 资源消耗:由于会阻塞线程,在大量IO操作时会创建大量线程导致系统资源消耗过大。

适用场景:

  • 并发量较小的应用。
  • 对实时性要求不高的场景。

使用演示:

// 写入file.txt文件
try (PrintWriter writer = new PrintWriter(new FileWriter("file.txt"))) {
    writer.println("Hello World");
} catch (IOException e) {
    e.printStackTrace();
}
// 读取file.txt文件
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    e.printStackTrace();
}

1.1 流(Stream)

流(Stream)是数据传输的抽象,代表了数据从一个地方到另一个地方的流动。流可分为两大类:

  • 字节流(Byte Streams):用于以字节为单位进行数据传输,适用于处理二进制数据,如文件读写。
    • InputStream:抽象类,提供读取字节的方法。
    • OutputStream:抽象类,提供写入字节的方法。
  • 字符流(Character Streams):用于以字符为单位进行数据传输,适用于处理文本数据。
    • Reader:抽象类,提供读取字符的方法。
    • Writer:抽象类,提供写入字符的方法。

1.2 缓冲(Buffering)

为了减少对底层I/O系统的调用次数,BIO通常会使用缓冲机制。缓冲流(如BufferedInputStreamBufferedOutputStream)可以在内存中暂存数据,从而减少实际的I/O操作次数。

2. NIO

NIO(Non-blocking IO)即非阻塞IO,是Java 1.4版本引入的一种IO模型。NIO旨在提供非阻塞的IO操作方式,这意味着线程在请求IO操作后可以立即返回,不必等待IO操作完成,在密集IO操作时极大提高了线程利用效率。

特点:通过选择器管理多个通道,非阻塞式效率较高,适用于连接数较多的场景

实现方式:通过选择器轮询多个通道的状态,当有通道就绪时就进行处理。

2.1 缓冲区(Buffer)

缓冲区是NIO中用于存储数据的区域,本质上是一块可以写入、读取数据的内存。NIO中的缓冲区(Buffer)实现了对数据有序和高效的管理,是进行数据传输的基础。对于不同的数据类型,NIO提供了不同的缓冲区,如ByteBufferCharBufferIntBuffer等,它们都继承自Buffer类。

缓冲区的基本属性

  • 容量(Capacity):缓冲区能够容纳的数据元素的最大数量。
  • 位置(Position):下一个要读取或写入的数据元素的索引。
  • 限制(Limit):缓冲区中第一个不应该读取或写入的元素的索引。
    • 当缓冲区处于写模式时,limit通常为缓冲区的capacity,代表可以写入的最大数据量。
    • 当缓冲区处于读模式时,limit通常为最后一次写入操作后的position,表示你可以读取的最大数据量。

缓冲区的常见方法

Buffer类中的通用方法:

  • capacity(): 返回缓冲区的容量。
  • position(): 返回缓冲区的位置。
  • limit(): 返回缓冲区的限制。
  • hasRemaining(): 告知当前位置和限制之间是否有元素。
  • remaining(): 返回当前位置和限制之间的元素数量。
  • mark(): 在当前位置设置标记。
  • reset(): 将位置重置为之前标记的位置。
  • rewind(): 重读缓冲区,将位置设置为0,限制不变。
  • clear(): 切换到写模式,清空缓冲区,将位置设置为0,限制设置为容量,并丢弃标记。
  • flip(): 切换到读模式,将限制设置到当前位置,将位置设置为0。
  • compact(): 压缩缓冲区,将未读数据复制到缓冲区的开始位置之后将定位设置到最后一个未读元素下一位。

分配缓冲区(ByteBuffer):

// 在Java堆中分配一个1024字节的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 直接在操作系统的内存中分配一个1024字节的缓冲区
// 直接缓冲区通常拥有更好的IO性能,但分配开销更大
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

写入数据到缓冲区(ByteBuffer):

buffer.put((byte)'a'); // 写入一个字节
buffer.put(new byte[10]); // 写入一个字节数组

从缓冲区读取数据(ByteBuffer):

byte b = buffer.get(); // 读取一个字节

2.2 通道(Channel)

通道是NIO中用于在缓冲区和位于通道另一侧的实体(通常是文件或套接字)之间传输数据的对象。与流的不同之处在于它是双向的,而流通常是单向的(例如,InputStream或OutputStream)。通道可以用于读写操作,而流通常只能用于读或写。

通道的顶级父类为Channel,提供了以下方法:

// 该方法用于检查通道是否处于打开状态
public boolean isOpen();
// 该方法用于关闭通道,释放与其关联的资源
public void close() throws IOException;

主要的通道类:

  • SocketChannel:用于TCP网络连接的数据读写。
  • ServerSocketChannel:用于监听和接受TCP连接请求。
  • FileChannel:用于文件的读写操作。
  • AsynchronousSocketChannel:异步TCP网络通信。
  • AsynchronousServerSocketChannel:异步接受TCP连接。
  • AsynchronousFileChannel: 异步文件读写操作。

使用演示(SocketChannel):

打开一个通道:

SocketChannel socketChannel = SocketChannel.open();
// 连接到远程地址
socketChannel.connect(new InetSocketAddress("localhost", 8080));

从管道中读写数据:

// 创建缓冲区并写入指定字节
ByteBuffer buffer = ByteBuffer.wrap("Hello, Server!".getBytes());
// 将缓冲区数据写入管道
// 实际写入字节可能小于缓冲区剩余字节所以需要循环写入
while (buffer.hasRemaining()) socketChannel.write(buffer);

ByteBuffer readBuffer = ByteBuffer.allocate(1024);
// 从管道读取数据到ByteBuffer,返回实际读取的字节数
while (socketChannel.read(readBuffer) > 0) {
    readBuffer.flip();
    // 读取数据
    ...
}

关闭通道:

socketChannel.close();

2.3 选择器(Selector)

选择器是NIO中的一个核心组件,它能够用单个线程监控多个通道,并知晓通道是否为诸如读写事件做好准备,从而极大减少线程数量,提高系统资源利用率。

选择器的工作原理如下:

  • 注册通道到选择器上,并指定感兴趣的事件(如连接就绪、数据可读、数据可写等)。
  • 调用选择器的select()方法,该方法会阻塞直到至少有一个注册的事件发生。
  • 处理发生的事件,例如,如果数据可读,则从通道读取数据。

3. AIO(重点)

AIO(Asynchronous IO)是NIO的一部分,使用另一种实现方式,是真正的异步IO。AIO也是通过通道来读写数据,但与NIO不同的是,AIO不需要选择器来轮询通道状态,而是由系统告知,这也就导致AIO需要系统的支持。

特点:

  • 事件驱动:AIO基于事件驱动模型,应用程序通过注册感兴趣的事件(如数据可读、可写等)来处理IO事件。
  • 异步操作:AIO允许应用程序发起非阻塞的IO操作。当IO操作完成时,操作系统会通知应用程序,这样应用程序可以在等待IO结果时执行其他任务。
  • CompletionHandler接口:在AIO中,CompletionHandler接口用于处理异步操作完成时的回调。当异步操作完成时,操作系统会调用CompletionHandler的实现方法。

使用演示(AsynchronousSocketChannel):

InetSocketAddress serverAddress = new InetSocketAddress("localhost", 8080);
// 开启异步网络通道
AsynchronousSocketChannel clientChannel = AsynchronousSocketChannel.open();
// 连接到指定IP
clientChannel.connect(serverAddress, null, new CompletionHandler<Void, Void>() {
    // 连接成功后的回调方法
    @Override
    public void completed(Void result, Void attachment) {
        ByteBuffer buffer = ByteBuffer.wrap("Hello, Server!".getBytes());
        // 发送消息到服务器
        clientChannel.write(buffer, null, new CompletionHandler<Integer, Void>() {
            // 发送成功后的回调方法
            @Override
            public void completed(Integer result, Void attachment) {
                buffer.clear();
                // 从服务器读取响应
                clientChannel.read(buffer, null, new CompletionHandler<Integer, Void>() {
                    // 响应完成后的回调方法
                    @Override
                    public void completed(Integer result, Void attachment) {
                        buffer.flip();
                        // 读取缓冲区
                        // ...
                        // 检查是否还有更多数据需要读取
                        if (result > 0 && buffer.hasRemaining()) 
                            clientChannel.read(buffer, null, this);
                    }
                    @Override
                    public void failed(Throwable exc, Void attachment) {
                        try { clientChannel.close(); }
                        catch (IOException e) { e.printStackTrace(); }
                    }
                });
            }
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("Failed to send data.");
                try { clientChannel.close(); }
                catch (IOException e) { e.printStackTrace(); }
            }
        });
    }
    @Override
    public void failed(Throwable exc, Void attachment) {
        System.out.println("Client failed to connect to the server.");
        try { clientChannel.close(); }
        catch (IOException e) { e.printStackTrace(); }
    }
});