NIO-缓冲区的分类和通道的使用

写在前面

  之前的一篇博客和大家一起学习了缓冲区最基本的使用,了解了缓冲区的 一些基本方法,大家应该都对NIO的高效有所耳闻,而阅读上一篇博客之后大家应该并没 有体会到NIO如何高效,那么阅读这篇博文之后大家应该就对NIO的高效有一个初步的了解 了。

  这篇博客会先和大家学习NIO缓冲区的两种分类———直接缓冲区和非直接缓冲区 ,之后在此基础上再来学习通道,并且用复制文件这个例子来体会缓冲区和通道的应用。

注:直接缓冲区的速度和效率要比非直接缓冲区以及传统IO操作高许多许多!!!

直接缓冲区和非直接缓冲区

  NIO的缓冲区分为两个基本类型———直接缓冲区和非直接缓冲区,上一篇博客我们 使用 allocate() 方法创建的缓冲区实际上就是非直接缓冲区,以读取本机物理磁盘上的一个 文件举例,非直接缓冲区是这样操作的(如图):

Markdown

  非直接缓冲区在进行读操作的时候,请求发出后,会先把物理磁盘的文件读取到 内核地址空间中,然后再复制到用户地址空间中(JVM)之后再从用户地址空间读取到应用程序中 来。写操作类似。(中间那条线左边是操作系统,右边如果你用的JAVA那就是JVM)

  OK,看了非直接缓冲区之后我们来看看直接缓冲区(如图)

Markdown

  我滴妈,咋好像比非直接缓冲区还要复杂?

Markdown

  其实不是的,中间的那个黑色的叉号是没有那个的意思,那其实直接缓冲区是这样的, 我们依然用读操作举个例子,当应用程序发送个读请求的时候,物理磁盘会直接把这个文件读到 物理内存的一个映射文件中来,然后应用程序直接从这个物理内存映射文件就把这个目标文件读 出来了, So,操作直接缓冲区的话就不需要像操作非直接缓冲区那样从内核地址空间和用户地址 空间打交道了,而是直接和本机的物理内存打交道,很明显,人家物理内存肯定比你JVM的效率要 高啊!

直接缓冲区的创建

  创建非直接缓冲区我们使用allocate()方法,而创建直接缓冲区其实只要使用allocateDirect() 方法就可以了,另外在JDK1.7之后还可以通过内存映射文件创建直接缓冲区。


  public void testDirect(){
    //分配直接缓冲区
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    //isDirect方法用于判断其是不是直接缓冲区
    System.out.println(buffer.isDirect());
  }

  直接缓冲区暂时说到这里,因为我们还没了解通道(Channel)呢啊,了解了通道之后就对缓冲区会有个 比较清晰的了解了。

通道(Channel)

  之前说过,缓冲区是用来存放数据的,而通道是负责传输的,所以,没有通道 的话,缓冲区存在的意义就不是很大了,所以下来讨论一下通道:

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

通道有如下主要实现类
  • FileChannel

  • SocketChannel

  • ServerSocketChannel

  • DatagramChannel

获取通道的方法:

1.Java针对支持通道的类提供了getChannel()方法,如下这些类都支持通道:

本地IO:

    FileInputStream/FileOutputStream

    RandomAccessFile

网络IO

    Socket

    ServerSocket

    DatagramSocket

调用该对象的 getChannel 方法,这会返回一个连接到相同底层文件的文件通道。

2.在JDK1.7以后Java针对各个通道提供了一个静态方法open()

3.在JDK1.7以后Java的Files工具类的newByteChannel()

NIO文件复制举例

  说了这么多,我们就来使用通道和缓冲区来实现一个简单的文件复制, 由此来体验一下NIO的使用。每段代码我都会在后面详细的说明。说明里会有代码中 所使用的方法的简单介绍,希望大家对照着代码阅读下说明。

非直接缓冲区


    @Test
    //利用通道完成文件的复制(非直接缓冲区)
    public void test1() throws Exception{
        //使用第一种获取通道的方法
        FileInputStream inputStream = new FileInputStream("1.jpg");
        FileOutputStream outputStream = new FileOutputStream("2.jpg");
        //①获取通道
        FileChannel inChannel = inputStream.getChannel();
        FileChannel outChannel = outputStream.getChannel();
        //②分配指定大小的缓冲区(非直接)
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //③将通道中的数据存入缓冲区中
        while(inChannel.read(buffer)!=-1){
            //④将缓冲区中的数据写入通道
            buffer.flip();
            outChannel.write(buffer);
            buffer.clear();
        }
        outChannel.close();
        inChannel.close();
        inputStream.close();
        outputStream.close();

    }


  说明:上面这段代码,目的是复制1.jpg到2.jpg(2.jpg是复制过程中新建的,复制钱并不存在),因 为上文中提到 FileInputStream/FileOutputStream 都是支持通道的类,所以调用这两个 类的 getChannel() 方法就可以得到针对 1.jpg 和 2.jpg 这两个 io 源的不同的两个通道, 分别是 inChannel 和 outChannel。

  然后创建一个字节缓冲区,调用 FileChannel.read(ByteBuffer dst) 方法,将字节序列从此通道读到指定的缓冲区中, 如果该通道已经到达流的末尾,会返回 -1 ,接着在循环中调用 flip() 方法调换这个buffer的当前位置,该方法会设置当前位置是0, 之后就从当前位置(下标为0),调用 FileChannel().write(ByteBuffer dst) 方法,将缓冲区中的字节写入 outChannel 对应的io源中。 之后清空缓冲区,循环的进行下一次读写操作,直到该通道已经到达流的末尾为止。

  最后要记得关闭所有的通道和流,这段代码其实会抛出IOException,我在上边throws掉了,日常开发中一定要try/catch调, 然后关闭流和缓冲区的操作要放到finally里面。

直接缓冲区

  接着我们来用直接缓冲区实现和上面同样的功能,在这里,我们使用FileChannel的open方法(JDK1.7+)获取通道, 使用FileChannel的map方法,通过内存映射文件创建直接缓冲区,这个两方法 在这里先简单的说下,不然阅读代码会有困难。

FileChannel的open方法:

方法原型:

  public static FileChannel open(Path path, OpenOption… options) throws IOException

目的:新建或打开一个指定io源的通道

返回:返回一个指定io源的通道

参数:

  1.Path path,这个参数用来指定这个io的源,一般通过Paths.get(“路径”),来获取,关于Paths这个类型,会在以后的博文中专门介绍。

  2.OpenOption… options, 这个参数大家应该也注意到了,它是一个可变参数,关于可变参数的具体应用,在这里就不多说了,简单的说就是 可以一个也可以多个,这个参数顾名思义就是打开或创建这个io源的方式,可以通过StandardOpenOption这个类来获取,具体有以下方式(如图):

Markdown

FileChannel的map方法:

方法原型:

  public abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) throws IOException

目的:将通道中指定的io源文件,直接映射到物理内存中。

返回:返回一个 MappedByteBuffer 类,该类表示一个由内存映射文件创建的直接字节缓冲区。该类是 ByteBuffer 的子类。

参数:

  1.FileChannel.MapMode mode,我们可以选择三种模式之一的方式被映射到内存中,这三种模式就是 FileChannel.MapMode 下的常量指定的, 具体分为:Read-only、Read-Write、Private三种类型。

  2.long position,指定从通道的哪个位置开始创建直接字节缓冲区。

  3.long size,创建的直接字节缓冲区的大小。


   @Test
   //利用通道完成文件的复制(直接缓冲区)
   public void test2() throws IOException{
       //使用第二种获取通道的方法
       FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
       FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE_NEW);
       //使用内存映射文件得到直接缓冲区
       MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY,0,inChannel.size());
       MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size());
       //直接对建立在计算机的物理内存中的缓冲区进行操作
       byte[] dst = new byte[inMappedBuf.limit()];
       inMappedBuf.get(dst);
       outMappedBuf.put(dst);

       inChannel.close();
       outChannel.close();

   }

  说明:上面这段代码先使用 FileChannel 的 open 得到针对 1.jpg 和 2.jpg 这两个 io 源的不同的两个通道, 之后使用内存映射文件得到了针对这两个通道的两个直接缓冲区,然后直接对建立在计算机的物理内存中的缓冲区进行操作,创建一个大小为 inMappedBuf 的限制长度大小的 byte 数组, 将inMappedBuf中的字节信息(1.jpg)写入到该字节数组中,之后将该字节数组中的字节信息put到outMappedBuf中(2.jpg),之后关闭通道连接即可。

最后说几句

  经过大量测试,直接缓冲区的效率要比非直接缓冲区高很多,但是,绝对不能滥用直接缓冲区,不然可能会有意想不到的惊喜, 具体何时使用,还是要根据实际情况进行测试之后再决定,NIO其实也有一些使用方面的弊端,这个之后的博文会专门讨论。


笔者水平有限,若有错漏,欢迎指正,如果转载以及CV操作,请务必注明出处,谢谢!
版权声明:本文为博主原创文章,未经博主允许不得转载。

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦