乐趣区

关于后端:让我对象告诉你什么是IONIOBIO

1、Stream 与 Channel

  • stream 不会主动缓冲数据,channel 会利用零碎提供的发送缓冲区、接收缓冲区(更为底层)
  • stream 仅反对阻塞 API,channel 同时反对阻塞、非阻塞 API,网络 channel 可配合 selector 实现多路复用
  • 二者均为全双工,即读写能够同时进行
  • 尽管 Stream 是单向流动的,然而它也是全双工的

2、IO 模型

  • 同步:线程本人去获取后果(一个线程)

    • 例如:线程调用一个办法后,须要期待办法返回后果
  • 异步:线程本人不去获取后果,而是由其它线程返回后果(至多两个线程)

    • 例如:线程 A 调用一个办法后,持续向下运行,运行后果由线程 B 返回

当调用一次 channel.read 或 stream.read 后,会由用户态切换至操作系统内核态来实现真正数据读取,而读取又分为两个阶段,别离为:

  • 期待数据阶段
  • 复制数据阶段

依据 UNIX 网络编程 – 卷 I,IO 模型次要有以下几种

阻塞 IO

  • 用户线程进行 read 操作时,须要期待操作系统执行理论的 read 操作,此期间用户线程是被阻塞的,无奈执行其余操作

非阻塞 IO

  • 用户线程

    在一个循环中始终调用 read 办法,若内核空间中还没有数据可读,立刻返回

    • 只是在期待阶段非阻塞
  • 用户线程发现内核空间中有数据后,期待内核空间执行复制数据,待复制完结后返回后果

多路复用

Java 中通过 Selector 实现多路复用

  • 当没有事件是,调用 select 办法会被阻塞住
  • 一旦有一个或多个事件产生后,就会解决对应的事件,从而实现多路复用

多路复用与阻塞 IO 的区别

  • 阻塞 IO 模式下,若线程因 accept 事件被阻塞,产生 read 事件后,仍需期待 accept 事件执行实现后,能力去解决 read 事件
  • 多路复用模式下,一个事件产生后,若另一个事件处于阻塞状态,不会影响该事件的执行

异步 IO

  • 线程 1 调用办法后了解返回,不会被阻塞也不须要立刻获取后果
  • 当办法的运行后果进去当前,由线程 2 将后果返回给线程 1

3、零拷贝

零拷贝指的是数据无需拷贝到 JVM 内存中,同时具备以下三个长处

  • 更少的用户态与内核态的切换
  • 不利用 cpu 计算,缩小 cpu 缓存伪共享
  • 零拷贝适宜小文件传输

传统 IO 问题

传统的 IO 将一个文件通过 socket 写出

File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");

byte[] buf = new byte[(int)f.length()];
file.read(buf);

Socket socket = ...;
socket.getOutputStream().write(buf);

外部工作流如下

  • Java 自身并不具备 IO 读写能力,因而 read 办法调用后,要从 Java 程序的 用户态切换至内核态 ,去调用操作系统(Kernel)的读能力,将数据读入 内核缓冲区。这期间用户线程阻塞,操作系统应用 DMA(Direct Memory Access)来实现文件读,其间也不会应用 CPU

    DMA 也能够了解为硬件单元,用来解放 cpu 实现文件 IO

  • 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte [] buf),这期间 CPU 会参加拷贝,无奈利用 DMA
  • 调用 write 办法,这时将数据从 用户缓冲区(byte [] buf)写入 socket 缓冲区,CPU 会参加拷贝
  • 接下来要向网卡写数据,这项能力 Java 又不具备,因而又得从 用户态 切换至 内核态 ,调用操作系统的写能力,应用 DMA 将 socket 缓冲区 的数据写入网卡,不会应用 CPU

能够看到中间环节较多,java 的 IO 理论不是物理设施级别的读写,而是缓存的复制,底层的真正读写是操作系统来实现的

  • 用户态与内核态的切换产生了 3 次,这个操作比拟重量级
  • 数据拷贝了共 4 次

NIO 优化

通过 DirectByteBuf

  • ByteBuffer.allocate(10)

    • 底层对应 HeapByteBuffer,应用的还是 Java 内存
  • ByteBuffer.allocateDirect(10)

    • 底层对应 DirectByteBuffer,应用的是操作系统内存

大部分步骤与优化前雷同,唯有一点:Java 能够应用 DirectByteBuffer 将堆外内存映射到 JVM 内存中来间接拜访应用

  • 这块内存不受 JVM 垃圾回收的影响,因而内存地址固定,有助于 IO 读写
  • Java 中的 DirectByteBuf 对象仅保护了此内存的虚援用,内存回收分成两步

    • DirectByteBuffer 对象被垃圾回收,将虚援用退出援用队列

      • 当援用的对象 ByteBuffer 被垃圾回收当前,虚援用对象 Cleaner 就会被放入援用队列中,而后调用 Cleaner 的 clean 办法来开释间接内存
      • DirectByteBuffer 的开释底层调用的是 Unsafe 的 freeMemory 办法
    • 通过专门线程拜访援用队列,依据虚援用开释堆外内存
  • 缩小了一次数据拷贝,用户态与内核态的切换次数没有缩小

进一步优化 1

以下两种形式都是零拷贝,即无需将数据拷贝到用户缓冲区中(JVM 内存中)

底层采纳了 linux 2.1 后提供的 sendFile 办法,Java 中对应着两个 channel 调用 transferTo/transferFrom 办法拷贝数据

  • Java 调用 transferTo 办法后,要从 Java 程序的 用户态 切换至 内核态 ,应用 DMA 将数据读入 内核缓冲区,不会应用 CPU
  • 数据从 内核缓冲区 传输到 socket 缓冲区,CPU 会参加拷贝
  • 最初应用 DMA 将 socket 缓冲区 的数据写入网卡,不会应用 CPU

这种办法下

  • 只产生了 1 次用户态与内核态的切换
  • 数据拷贝了 3 次

进一步优化 2

linux 2.4 对上述办法再次进行了优化

  • Java 调用 transferTo 办法后,要从 Java 程序的 用户态 切换至 内核态 ,应用 DMA 将数据读入 内核缓冲区,不会应用 CPU
  • 只会将一些 offset 和 length 信息拷入 socket 缓冲区,简直无耗费
  • 应用 DMA 将 内核缓冲区 的数据写入网卡,不会应用 CPU

整个过程仅只产生了 1 次用户态与内核态的切换,数据拷贝了 2 次

4、AIO

AIO 用来解决数据复制阶段的阻塞问题

  • 同步意味着,在进行读写操作时,线程须要期待后果,还是相当于闲置
  • 异步意味着,在进行读写操作时,线程不用期待后果,而是未来由操作系统来通过回调形式由另外的线程来取得后果

异步模型须要底层操作系统(Kernel)提供反对

  • Windows 零碎通过 IOCP 实现了真正的异步 IO
  • Linux 零碎异步 IO 在 2.6 版本引入,但其 底层实现还是用多路复用模仿了异步 IO,性能没有劣势

本文由 传智教育博学谷 教研团队公布。

如果本文对您有帮忙,欢送 关注 点赞 ;如果您有任何倡议也可 留言评论 私信,您的反对是我保持创作的能源。

转载请注明出处!

退出移动版