• Java I/O(3):NIO中的Buffer


    您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来~

     

    之前在调用Channel的代码中,使用了一个名叫ByteBuffer类,它是Buffer的子类。这个叫Buffer的类是专门用来解决高速设备与低速设备之间速度不匹配的问题的,也可以减少数据库的读写次数。

    它又分为输入缓冲区和输出缓冲区。

    很多初学者不明白「缓冲」和「缓存」的区别,我尝试着用大白话解释下:

    1、缓冲区需要定期进行刷新、清空、重置等操作,这些操作缓存可能并不需要。比如做饭时,砧板就是缓冲,冰箱就是缓存,因为从菜冰箱取出来到下锅,需要不停地切、拍、剁,每次都要清空了才能做下一道菜,而冰箱是不用定期清空、重置的(除非停电,东西都坏了);

     

     

    2、缓冲区核心作用是解耦设备间的速度制约,成为设备间的「缓冲」,而缓存则是用来加快读取的速度,减少重新计算或者重新从数据库获取的次数。相比于每做一道菜,都从菜场去买,显然放在冰箱要快得多;而相比于每次做菜都从冰箱拿,当然从砧板上顺手拿要更快一些。也就是:「菜场买菜速度(磁盘 < 冰箱拿菜速度(缓存 < 砧板拿菜速度(缓冲区)」,就是这么个关系;

    3、缓冲区侧重于速度,侧重于写,而缓存侧重次数,侧重于读。就像砧板侧重于切菜,而冰箱侧重于存放;

    4、现在的缓存一般都很大,甚至可以达到TB级别1TB=1024GB),缓冲是不可能这么大的(当然你也可以把砧板搞成冰箱那么大,反正我还没见过这种-_-!)。

     

    以后再见到缓冲、缓存的时候,就可以拿家里的砧板和冰箱做对比。

     

    NIO中有八种类型的缓冲区:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer, ShortBuffer和MappedByteBuffer,前七种分别对应基本数据类型,MappedByteBuffer专门用于内存映射。

     

     

     

    缓冲Buffer区实际上也是一个容器,一个由连续数组/集合组成的容器。Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。

    Buffer中写入数据的过程是:

    1、Channel写入数据到Buffer:channel.read(buf)

    2、调用Buffer的put()方法:buf.put(Object)

    Buffer中读取数据的过程是:

    1、Buffer读取数据到Channel:channel.write(buf)

    2、调用Buffer的get()方法:buf.get()

    读写过程大概就是这样的:

     

     

     

    还是昨天那句话:如果你在大厂是自研类RPC系统或类MQ中间件的,那这个一定要精通;否则理解就好,不必死磕。Buffer看到这里其实已经足够了。至于说:Buffer的属性、使用Buffer的步骤、JVM怎么在内存创建缓冲区等等,这些应该都是面霸必修课,但开发中极少用到。

    还是用代码来说。

    Buffer的常用方法:

    复制代码
    // 分配JVM间接缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(32);
    System.out.println("buffer初始状态: " + buffer);
    // 将position设回8
    buffer.position(8);
    System.out.println("buffer设置后状态: " + buffer);
    
    System.out.println("测试reset ======================>>>");
    // clear()方法,position将被设回0,limit被设置成capacity的值
    buffer.clear();
    System.out.println("buffer clear后状态: " + buffer);
    // 设置这个缓冲区的位置
    buffer.position(5);
    // 将此缓冲区的标记设置5
    // 如果没有buffer.mark();这句话会报错
    buffer.mark();
    buffer.position(10);
    System.out.println("reset前状态: " + buffer);
    // 将此缓冲区的位置重置为先前标记的位置(buffer.position(5))
    buffer.reset();
    System.out.println("reset后状态: " + buffer);
    
    System.out.println("测试get ======================>>>");
    buffer = ByteBuffer.allocate(32);
    buffer.put((byte) 'x').put((byte) 'i').put((byte) 'a').put((byte) 'n').put((byte) 'g');
    System.out.println("flip前状态: " + buffer);
    // 转换为读模式
    buffer.flip();
    System.out.println("get前状态: " + buffer);
    System.out.println((char) buffer.get());
    System.out.println("get后状态: " + buffer);
    
    System.out.println("测试put ======================>>>");
    ByteBuffer pb = ByteBuffer.allocate(32);
    System.out.println("put前状态: " + pb +
            ", put前数据: " + new String(pb.array()));
    System.out.println("put后状态: " + pb.put((byte) 'w') +
            ", put后数据: " + new String(pb.array()));
    System.out.println(pb.put(3, (byte) '3'));
    // put(3, (byte) '3')并不改变position的位置,但put((byte) '3')会
    System.out.println("put(3, '3')后状态: " + pb + ", 数据: " + new String(pb.array()));
    // 这里的buffer是 xiang[pos=1 lim=5 cap=32]
    System.out.println("buffer叠加前状态: " + buffer +
            ", buffer叠加前数据: " + new String(buffer.array()));
    // buffer.put(pb);会抛异常BufferOverflowException
    pb.put(buffer);
    // 叠加后数据是wiang,因为buffer的position=1
    System.out.println("put(buffer)后bb状态: " + pb + ", buffer叠加后数据: " + new String(pb.array()));
    
    // 重新读取buffer中所有数据
    System.out.println("测试rewind ======================>>>");
    buffer.clear();
    buffer.position(10);
    System.out.println("buffer当前状态: " + buffer);
    // 返回此缓冲区的限制
    buffer.limit(15);
    System.out.println("limit后状态: " + buffer);
    // 把position设为0,mark设为-1,不改变limit的值
    buffer.rewind();
    System.out.println("rewind后状态: " + buffer);
    
    // 将所有未读的数据拷贝到Buffer起始处,然后将position设到最后一个未读元素正后面
    System.out.println("测试compact ======================>>>");
    buffer.clear();
    buffer.put("abcd".getBytes());
    System.out.println("compact前状态: " + buffer);
    System.out.println(new String(buffer.array()));
    // limit=position;position=0;mark=-1; 翻转,也就是让flip之后的position到limit这块区域变成之前的0到position这块
    // 翻转就是将一个处于存数据状态的缓冲区变为一个处于准备取数据的状态,或者相反
    buffer.flip();
    System.out.println("flip后状态: " + buffer);
    // get()方法:相对读,从position位置读取一个byte,并将position+1,为下次读写作准备
    System.out.println((char) buffer.get());
    System.out.println((char) buffer.get());
    System.out.println((char) buffer.get());
    System.out.println("三次调用get后: " + buffer);
    System.out.println(new String(buffer.array()));
    // 把从position到limit中的内容移到0到limit-position的区域内
    // position和limit的取值也分别变成limit-position、capacity
    // 如果先将positon设置到limit,再compact,那么相当于clear()
    buffer.compact();
    System.out.println("compact后状态: " + buffer);
    System.out.println(new String(buffer.array()));
    复制代码

     

    Java一般用BufferedInputStream、BufferedReader等带缓冲的I/O类来处理大文件,但如果文件超大的话,比如达到GB,甚至TB级别,更快的方式是采用NIO中引入的文件内存映射方案:MappedByteBuffer。

    你只需要MappedByteBuffer读写性能极高,最主要的原因就是因为它实现了对异步操作的支持,就可以了!

    可以用大文件来试一下:

    复制代码
    // ByteBuffer读取大文件
    public static void useFileChannel() {
        try{
            FileInputStream fis = new FileInputStream("你电脑上已经存在的文件路径,例如C:\\file1");
            FileChannel channel = fis.getChannel();
            long start = System.currentTimeMillis();
            ByteBuffer buff = ByteBuffer.allocate((int) channel.size());
            buff.clear();
            channel.read(buff);
            long end = System.currentTimeMillis();
            System.out.println(end - start);
            fis.close();
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // MappedByteBuffer读取大文件
    public static void useMappedByteBuffer() {
        try{
            FileInputStream fis = new FileInputStream("你电脑上已经存在的文件路径,例如C:\\file1");
            FileChannel channel = fis.getChannel();
            long start = System.currentTimeMillis();
            MappedByteBuffer mbb = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
            long end = System.currentTimeMillis();
            System.out.println(end - start);
            fis.close();
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        useFileChannel();
        useMappedByteBuffer();
    }
    复制代码

     

    最后把这两个方法放到main()里面试试看效果。

     

    NIO中的Buffer说这么多已经足够了,用代码去感受会更直接。

     


     

     

    感谢您的大驾光临!咨询技术、产品、运营和管理相关问题,请关注后留言。欢迎骚扰,不胜荣幸~

     

  • 相关阅读:
    Stream 的使用,我觉得使用它是非常方便的
    RHCE(四)--- DNS服务的正反向解析配置
    FasterNet(PConv)paper笔记(CVPR2023)
    Java Cache 缓存方案详解及代码-Ehcache
    事件循环
    LLM 大模型学习必知必会系列(三):LLM和多模态模型高效推理实践
    BGP联邦实验
    Vue2.0开发之——Vue基础用法-axios(29)
    简单的3D协作工具,Horizon Worlds也能用来开发VR游戏?
    8年测试经验之谈 —— 什么是全链路压测?
  • 原文地址:https://www.cnblogs.com/xiangwang1111/p/16807925.html