NIO 与通道数据传输

NIO 的通道

通道 (Channel) :由 java.nio.channels 包定义的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的 “流”, 只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。

应用程序与磁盘之间的数据写入或者读出,都需要由用户地址空间和内存地址空间之间来回复制数据,内存地址空间中的数据通过操作系统层面的 IO 接口,完成与磁盘的数据存取。在应用程序调用这些系统 IO 接口时,由 CPU 完成一系列调度、任务分配,在早期这些 IO 接口都是由 CPU 独立负责。所以当发生大规模读写请求时,CPU 的占用率很高。

mark

之后,操作系统为了避免 CPU 完全被各种 IO 接口调用占用,引入了 DMA,也就是直接存储器。当应用程序对操作系统发出一个读写请求时,会由 DMA 先向 CPU 申请权限,申请到权限之后,内存地址空间与磁盘之间的 IO 操作就全由 DMA 来负责操作。这样,在读写请求的过程中,CPU 不需要再参与,CPU 去做其他事情。当然,DMA 来独立完成数据在磁盘与内存空间中的复制,需要借助于 DMA 总线。当频繁发生 IO 操作时,会导致 DMA 总线增多,但是当 DMA 总线过多时,大量的 IO 操作也会造成总线冲突的问题,即也会影响最终的读写性能。

mark

为了避免 DMA 总线冲突对性能的影响,后来便有了通道的方式。通道,它是一个完全独立的处理器。CPU 是中央处理器,通道本身也是一个处理器,完全独立的处理器,但是也附属于 CPU,专门负责 IO 操作。既然是处理器,通道有自己的 IO 命令,与 CPU 无关。它更适用于大型的 IO 操作,性能更高。

mark

直接存储器 DMA 有独立总线。但在大量数据面前,可能会存在总线冲突,还是需要 CPU 来处理。通道是一个独立的处理器,DMA 方式还是需要向 CPU 申请 DMA 总线的。通道有自己的处理器,适合与大量 IO 请求的场景,数据传输直接通过通道进行传输,不再需要请求 CPU,这样便避免了大量 IO 导致 CPU 资源占用过高的问题!

NIO 完成文件复制

Channel 用于源节点与目标节点的连接,在 JavaNIO 中负责缓冲区数据的传输,本身不存储数据,所以需要配置缓冲区进行数据传输(铁路配合火车)

一、通道的主要实现类

实现的是 java.nio.channels.Channel 接口

  • FileChannel 文件 IO
  • SocketChannel TCP 的 IO
  • ServerSocketChannel TCP 的 IO
  • DatagramChannel UDP 的 IO

二、获取通道

1、对支持通道的类提供了 getChannel ()

  • 本地 IO
  • FileInputStream/FileOutputStream
  • RandomAccessFile
  • 网络 IO
  • Socket
  • ServerSocket
  • DatagramChannel

2、JDK1.7 中的 NIO.2 针对各种通道提供了静态方法

3、JDK1.7 中的 NIO.2 的 Files 工具类的 newByteChannel ()

1、FileInputStream 打开的通道,通过通道 + 非直接缓冲区完成文件复制

mark

2、这是通过把缓冲区建立在物理内存中的做法,使用了 FileChannel 的静态方法打开的通道

mark

3、这是使用直接操作缓冲区完成的文件复制

mark

4、这是不建立缓冲区,直接通道数据传输

mark

我试了一下,复制 1.3G 的压缩包,四种方式分别用时为:

1、通道 + 非直接缓冲区完成文件复制:5956、 5645(内存有波动,不明显)

2、通道 + 直接缓冲区完成文件复制:5439、5302(内存有波动,不明显)

3、直接操作缓冲区完成的文件复制:1581、1560、1523(内存波动特别明显,见下图)

4、这是不建立缓冲区,直接通道数据传输:704、685、615 (内存几乎无波动)

mark

得出结论:直接缓冲区肯定比非直接缓冲区快,但是我这次测试效果不是很明显,直接操作内存映射文件速度明显提升,但是也会导致内存突然爆涨(因为是直接操作的内存映射文件),方式 4 就更狠了,直接通道传输数据,压根与内存没关系,因为没有缓冲区的存在,所以也是最快的!

分散读取和聚集写入

分散读取 ( Scattering Reads) 是指从 Channel 中读取的数据 “分散” 到多个 Buffer 中。

mark

注意:按照缓冲区的顺序,从 Channel 中读取的数据依次将 Buffer 填满。

聚集写入( Gathering Writes)是指将多个 Buffer 中的数据 “聚集” 到 Channel 中,与分散读取是反的。 注意:按照缓冲区的顺序,写入 position 和 limit 之间的数据到 Channel 。

mark

下面是一个分散读取 & 聚集写入的示例:

mark

分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中

聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中

字符集 Charset

只要是需要知道 CharBuffer 和 ByteBuffer 的相互装换即可

mark

FileChannel 常用的 API

mark