欢迎光临BDM
一枚菜鸟码农的成仙之路

Java BIO NIO AIO 简述

什么是 Java I/O 系统?

Java 开发中,时常我们会用到除了 JVM 内存之外的数据,从数据库、文件或者网络中读取(通常情况下数据库也属于网络)。这些与外部数据源交互的方式,就是 I/O(In and Out)。Java 提供了一整套详尽而完善的 I/O API 来帮助开发者进行 I/O 操作,以传统的流式 API 为例,有如下的分类:

  • 读取、写入
  • 顺序、随机
  • 缓冲
  • 二进制、按字符、按行、按字
  • ……

Java I/O 系统进行了漫长的发展,都没有超出上述的需求,而是从使用方式和底层实现进行了优化和升级。Java I/O 系统总共经历了三个时代:

  • Java 1.0 的 BIO (Blocking I/O),同步阻塞 I/O
  • Java 1.4 的 NIO (New I/O),同步非阻塞 I/O
  • Java 1.7 的 AIO (Asynchronous I/O),异步非阻塞 I/O

BIO是传统的 Java 阻塞 I/O,速度最慢,以流的形式提供 API,功能强大而使用繁琐。NIO 将 I/O 交大部分的内容交由操作系统底层实现,所以速度快于 BIO,但 NIO 需手动实现同步非阻塞的轮询过程,使用起来同样麻烦。AIO 实现了异步 I/O,使用极其方便,但增加了多线程的开销。

在介绍这些 API 之前,我想先介绍一下其概念和原理,有助于更完美地运用 I/O 系统,减少使用过程中的误解和困惑。

什么是同步和异步、阻塞和非阻塞?

一个首当其冲的问题,到底什么是同步和异步、阻塞和非阻塞呢?这个问题曾经给初学的我带来了很大的困扰,究其原因,这类对立的描述在不同的语境下都有着略微不同的含义。尽管这些描述都包含着大部分的共同点,但那细微的区别,可能是引起困惑的主要原因,所以我们一定先把理解范畴局限于 Java 中。看了很多文章之后,我姑且总结如下:

同步:由调用方主动执行 I/O、数据获取和缓存

异步:由调用方执行 I/O,第三方帮调用方获取数据和缓存

阻塞:调用方会进行阻塞,直至 I/O 完成

非阻塞:调用方立即会获得返回,无论 I/O 是否完成

以我去饭店吃饭为例来表达这个差异:

同步:我去饭店吃饭,打饭得自己来

异步:我去饭店吃饭,我向服务员要饭,服务员把饭送到我手上

阻塞:饭店顾客太多,吃饭得站外面排队

非阻塞:饭店顾客太多,吃饭得先叫号,轮到我的号就可以进行吃饭

这里的就是调用方,打饭是 I/O 操作,服务员是第三方。两两结合后,应该是这样的:

同步阻塞:我去饭店吃饭,打饭得自己来,并且人多时需要排队

同步非阻塞:饭还没做好,于是我隔一会儿去看一下,饭做好了就打饭

异步非阻塞:饭还没做好,于是我告知服务员做好后送给我

异步阻塞在大部分语境下是不成立的,至少在 Java 语境下如此。Java 中,异步等同于另立了一个线程来执行任务,异步用来描述某个 API 时意味着该 API 一定能立即返回。但阻塞代表着当前线程执行某 API 时会遇到阻塞,直到完成所有工作才能返回,并继续执行。异步生成的线程哪怕遇到了阻塞,也无法对主线程造成影响,主线程无论如何不会因之而阻塞,所以异步阻塞是不存在的。

无论是同步、异步还是阻塞、非阻塞,他们都是在 API 上或者说高层次上的体现,而底层甚至到内核是如何运转都可能完全不同。不要太在乎他的称呼,只要弄清楚他的实现原理,便能灵活运用来实现我们的需求。

什么是 Java 中的 BIO NIO AIO?

首先看三张经典 UNIX I/O 模型的图片,Java 的三代 I/O 模型也是依赖于这个基础来实现。

Java BIO 底层模型

BIO

BIO 的流程中,首先线程调用 API 发起 I/O 请求,然后该线程进入 BLOCKING 状态,直至 I/O 操作全部完成。

实际 I/O 流程中,JVM 通知内核进行 I/O,内核将数据放在了内核空间,然后 JVM 主动将内核空间的数据移至自己的内存中(事件机制或者轮询机制,这点我暂时没有深究),再改变线程的状态,使其继续运行。

Java NIO 底层模型

NIO

NIO 的流程中,首先线程调用 API 发起 I/O 请求后,需要不断轮询是否完成 I/O,轮询时能立即获得当前已传输的部分数据,并对之执行逻辑。

NIO 在 BIO 的基础上新增加了 Channel 机制,依赖于操作系统内核。在 BIO 中,由 JVM 主动将内核空间中的数据移植用户空间(JVM 进程)中,而 NIO 为内核自动将数据同步至用户空间(JVM 进程)。

NIO 新增 Zero 复制,由 JVM 调用内核的复制功能在内核空间进行复制,无需加载至用户空间中。

NIO 实现了虚拟内存映射,将内核空间地址和用户空间地址进行映射绑定,使得 DMA 硬件(只能访问物理内存地址)能直接对 JVM 内存进行访问。

Java AIO 底层模型

AIO

AIO 在 NIO 的 Channel 上,新增了异步的 API。线程发起异步 I/O 请求时,将指定回调的任务,该任务由两个方法组成:I/O 执行成功和 I/O 执行失败,当 I/O 完成后,则会依据结果来执行这两个方法中的一个。

实际上,AIO 会使用另一个线程来执行回调任务,故在普通场景中,由于线程上下文切换性能有所损耗反而使效率降低。

AIO 同样依赖于操作系统内核的异步机制来实现,实现了 Proactor 模式。Proactor 模式中,应用程序需要传递缓存区,也就是 NIO 中的 Buffer,但与 NIO 不同的是,该 Buffer 需要足够大来容纳所有数据。

Java NIO 多路复用模型

NIO3

NIO 多路复用模型是使用 Selector 来实现的,Selector 可以简单地理解为 Channel 的容器,并且提供了一些简单管理的 API。Selector 内部对 Channel 用 SelectionKey 进行了封装,一一对应,使 SelectionKey 能包含更多的信息,Selector 通过内部 SelectionKey 的轮询来获取 Channel 的状态来实现需求,但这一切需要手工编码来实现 。

很多人这种模型称之为异步阻塞模型,我并不认同,对于它的 API 来说,它是阻塞的;对于运行状态来说,它是同步的(运行在当前线程上)。但它通常使用方式是以另一个线程轮询开启 Selector 监听(谁也不会让一个专职阻塞的 I/O 运行在主线程上),所以他被理解为异步阻塞模型。

Java 中的 BIO NIO AIO 简单实践

BIO 读取文件

@Test
@DisplayName("Java BIO file read")
void bio() throws IOException {
    // 创建文件输入流【对接操作系统,获取共享锁】
    try(FileInputStream fileInputStream = new FileInputStream(filePath)) {
        // 新建输入接收区,用于缓冲,非必需
        byte[] fileBuff = new byte[fileInputStream.available()];
        // 文件读取到接收区
        int fileLength = fileInputStream.read(fileBuff);
        // 将 byte[] 转化为String,以供输出
        String fileStr = new String(fileBuff);
        // 打印输出
        System.out.println("文件长度:" + fileLength);
        System.out.println("文件内容:" + fileStr);
    }
}

BIO 的流式读取比起另外两种很大的特点是不需要缓冲区,当调用 read 方法时会直接返回一个字节或者字符的单个或者数组,这可以节省到内存,但也失去了缓冲区的灵活性。

由于阻塞的特性,BIO 在 Socket 中会阻塞端口,占用端口资源。

NIO 读取文件

@Test
@DisplayName("Java NIO file read with ByteBuffer")
void nio() throws IOException {
    // 创建随机文件读写器,读+写模式,非流
    // 获取文件 Channel
    try (RandomAccessFile file = new RandomAccessFile(filePath, "rw");
         FileChannel inChannel = file.getChannel();){
        // 初始化 NIO 的 Buffer,此处为 Byte 类型的 Buffer
        ByteBuffer buf = ByteBuffer.allocate(48);
        // 定义数字用于接受每次读取的字节or字符数
        int bytesRead;
        // 读取到缓冲区,并判断读取长度
        while((bytesRead = inChannel.read(buf)) != -1) {
            System.out.println("本次循环读取字节数:" + bytesRead);
            // 将 Buffer 由写模式改为读模式
            buf.flip();
            // 判断是否存在待读取内容
            while (buf.hasRemaining()) {
                // 每次读一个字节/字符
                System.out.print((char) buf.get());
            }
            // 将 Buffer 由读模式改为写模式
            buf.clear();
        }
    }
}

NIO 通过创建 BIO 的输入输出类来获取 NIO 的 Channel,然后对 Channel 进行操作,Channel 依赖于 Buffer 缓冲区。

较之 BIO,NIO 必须指定 ByteBuffer 缓冲区,并且 NIO 需要复杂的轮询控制,并手动操作 ByteBuffer 的状态来使其正常运行。关于 Buffer 的学习可以参见 http://ifeve.com/buffers/。

文件操作在 Unix 下因其设计,无论如何都会被阻塞,无法实现真正的 NIO。

NIO 的 Selector 专用于网络 I/O ,无法适用于文件 I/O,实现方式可以理解为使用观察者模式维护了一个 Channel 的列表并且对其进行轮询,代码较多,这里暂且不贴。

AIO 读取文件

@Test
@DisplayName("Java AIO file read with ByteBuffer")
void aio() throws IOException, InterruptedException {
    // 获取文件 Channel,这里和 NIO 类似,都是面向 Channel 的,但 AIO 使用是异步 Channel
    // 创建 Channel 的过程中,文件
    AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(Paths.get(filePath));
    // 创建缓存区
    ByteBuffer byteBuffer = ByteBuffer.allocate(48);
    asynchronousFileChannel.read(byteBuffer, 0, "AIO Test", new CompletionHandler<Integer, String>() {
        // 读取成功
        @Override
        public void completed(Integer result, String attachment) {
            System.out.println(Thread.currentThread().getName() + " 读取成功!");
            System.out.println("本次读取字节数:" + result);
            // 将 Buffer 由写模式改为读模式
            byteBuffer.flip();
            // 获取数据,并打印
            byte[] data = new byte[byteBuffer.limit()];
            byteBuffer.get(data);
            System.out.println("本次读取内容:" + new String(data));
            // 将 Buffer 由读模式改为写模式
            byteBuffer.clear();
        }

        // 读取失败
        @Override
        public void failed(Throwable exc, String attachment) {
            System.out.println("read error");
        }
    });
}

AIO 较之 NIO 不需要控制轮询,代码非常清晰简洁。

使用 AIO 时一定注意 ByteBuffer 的大小一定要大于数据长度。在 NIO 中,哪怕 ByteBuffer 设置很小,也可以通过多次轮询把数据全部取出来,但是在 AIO 中,completed 方法只会执行以此,超出缓冲区大小的数据将会被遗弃。

本文遵守知识共享署名-相同方式共享 4.0 国际许可协议,未经允许不得转载暂时没有标题 » Java BIO NIO AIO 简述

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

联系我们GitHub