Java NIO之Buffer(缓冲区)

Java NIO主要解决了Java IO的效率问题,解决此问题的思路之一是利用硬件和操作系统直接支持的缓冲区、虚拟内存、磁盘控制器直接读写等优化IO的手段;思路之二是提供新的编程架构使得单个线程可以控制多个IO,从而节约线程资源,提高IO性能。
Java IO引入了三个主要概念,即缓冲区(Buffer)、通道(Channel)和选择器(Selector),本文主要介绍缓冲区。

  1. 缓冲区概念
    缓冲区是对Java原生数组的对象封装,它除了包含其数组外,还带有四个描述缓冲区特征的属性以及一组用来操作缓冲区的API。缓冲区的根类是Buffer,其重要的子类包括ByteBuffer、MappedByteBuffer、CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer。从其名称可以看出这些类分别对应了存储不同类型数据的缓冲区。

1.1四个属性
缓冲区由四个属性指明其状态。
容量(Capacity):缓冲区能够容纳的数据元素的最大数量。初始设定后不能更改。
上界(Limit):缓冲区中第一个不能被读或者写的元素位置。或者说,缓冲区内现存元素的上界。
位置(Position):缓冲区内下一个将要被读或写的元素位置。在进行读写缓冲区时,位置会自动更新。
标记(Mark):一个备忘位置。初始时为“未定义”,调用mark时mark=positon,调用reset时position=mark。
这四个属性总是满足如下关系:

mark<=position<=limit<=capacity

如果我们创建一个新的容量大小为10的ByteBuffer对象如下图所示:

image.png

在初始化的时候,position设置为0,limit和 capacity被设置为10,在以后使用ByteBuffer对象过程中,capacity的值不会再发生变化,而其它两个个将会随着使用而变化。三个属性值分别如图所示:


image.png

现在我们可以从通道中读取一些数据到缓冲区中,注意从通道读取数据,相当于往缓冲区中写入数据。如果读取4个字节的数据,则此时position的值为4,即下一个将要被写入的字节索引为4,而limit仍然是10,如下图所示:


image.png

下一步把读取的数据写入到输出通道中,相当于从缓冲区中读取数据,在此之前,必须调用flip()方法,该方法将会完成两件事情:

  1. 把limit设置为当前的position值
  2. 把position设置为0
    由于position被设置为0,所以可以保证在下一步输出时读取到的是缓冲区中的第一个字节,而limit被设置为当前的position,可以保证读取的数据正好是之前写入到缓冲区中的数据,如下图所示:


    image.png

现在调用get()方法从缓冲区中读取数据写入到输出通道,这会导致position的增加而limit保持不变,但position不会超过limit的值,所以在读取我们之前写入到缓冲区中的4个自己之后,position和limit的值都为4,如下图所示:


image.png

在从缓冲区中读取数据完毕后,limit的值仍然保持在我们调用flip()方法时的值,调用clear()方法能够把所有的状态变化设置为初始化时的值,如下图所示:


image.png

下面这个例子可以展示buffer的读写:

public class NioTest1 {

    public static void main(String[] args) {
        //通过nio生成随机数,然后在打印出来

        IntBuffer buffer = IntBuffer.allocate(10);
        System.out.println("capacity:"+buffer.capacity());
        for (int i = 0;i < 5;i++){
            int randomNumber = new SecureRandom().nextInt(20);
            //这里相当于把数据写到buffer中
            buffer.put(randomNumber);
        }

        System.out.println("before flip limit:"+buffer.capacity());

        //上面是写,下面为读,通过flip()方法进行读写的切换
        buffer.flip();

        System.out.println("after flip limit:"+buffer.capacity());

        System.out.println("enter while loop");

        while(buffer.hasRemaining()){
            System.out.println("position:" + buffer.position());
            System.out.println("limit:" + buffer.limit());
            System.out.println("capacity:" + buffer.capacity());

            //这里相当于从buffer中读出数据
            System.out.println(buffer.get());
        }

    }

1.3 remaining和hasRemaining
remaining()会返回缓冲区中目前存储的元素个数,在使用参数为数组的get方法中,提前知道缓冲区存储的元素个数是非常有用的。
事实上,由于缓冲区的读或者写模式并不清晰,因此实际上remaining()返回的仅仅是limit – position的值。
而hasRemaining()的含义是查询缓冲区中是否还有元素,这个方法的好处是它是线程安全的。

1.4 Flip翻转
在从缓冲区中读取数据时,get方法会从position的位置开始,依次读取数据,每次读取后position会自动加1,直至position到达limit处为止。因此,在写入数据后,开始读数据前,需要设置position和limit的值,以便get方法能够正确读入前面写入的元素。
这个设置应该是让limit=position,然后position=0,为了方便,Buffer类提供了一个方法flip(),来完成这个设置。其代码如下:

/**
     * 测试flip操作,flip就是从写入转为读出前的一个设置buffer属性的操作,其意义是将limit=position,position=0
     */
    private static void testFlip() {
       CharBuffer buffer = CharBuffer.allocate(10);
       buffer.put("abc");
       buffer.flip();
       char[] chars = new char[buffer.remaining()];
       buffer.get(chars);
       System.out.println(chars);

        //以下操作与flip等同
       buffer.clear();
       buffer.put("abc");
       buffer.limit(buffer.position());
       buffer.position(0);
       chars = new char[buffer.remaining()];
       buffer.get(chars);
        System.out.println(chars);
}

1.5compact压缩
压缩compact()方法是为了将读取了一部分的buffer,其剩下的部分整体挪动到buffer的头部(即从0开始的一段位置),便于后续的写入或者读取。其含义为limit=limit-position,position=0,测试代码如下:

 private static void testCompact() {
       CharBuffer buffer = CharBuffer.allocate(10);
       buffer.put("abcde");
       buffer.flip();
        //先读取两个字符
       buffer.get();
       buffer.get();
       showBuffer(buffer);
        //压缩
       buffer.compact();
        //继续写入
       buffer.put("fghi");
       buffer.flip();
        showBuffer(buffer);
        //从头读取后续的字符
       char[] chars = new char[buffer.remaining()];
       buffer.get(chars);
       System.out.println(chars);
}

1.6duplicate复制
复制缓冲区,两个缓冲区对象实际上指向了同一个内部数组,但分别管理各自的属性。

    private static void testDuplicate() {
        CharBuffer buffer = CharBuffer.allocate(10);
       buffer.put("abcde");
       CharBuffer buffer1 = buffer.duplicate();
       buffer1.clear();
       buffer1.put("alex");
       showBuffer(buffer);
       showBuffer(buffer1);
}

1.7 slice缓冲区切片

缓冲区切片,将一个大缓冲区的一部分切出来,作为一个单独的缓冲区,但是它们公用同一个内部数组。切片从原缓冲区的position位置开始,至limit为止。原缓冲区和切片各自拥有自己的属性,测试代码如下:

  /**
 * slice Buffer 和原有Buffer共享相同的底层数组
 */
public class NioTest6 {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        for (int i=0;i<buffer.capacity();i++){
            buffer.put((byte)i);
        }
        buffer.position(2);
        buffer.limit(6);

        ByteBuffer sliceBuffer = buffer.slice();
        for (int i=0;i < sliceBuffer.capacity();i++){
            byte b = sliceBuffer.get(i);
            b *= 2;
            sliceBuffer.put(i,b);
        }

        buffer.position(0);
        buffer.limit(buffer.capacity());

        while(buffer.hasRemaining()){
            System.out.println(buffer.get());
        }
    }
}

1.8只读Buffer
我们可以随时将一个普通Buffer调用asReadOnlyBuffer方法返回一个只读Buffer,但不能将一个只读Buffer转换成读写Buffer

public class NioTest7 {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println(buffer.getClass());
        for (int i=0;i<buffer.capacity();i++){
            buffer.put((byte)i);
        }

        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass());

        //只读buffer,不可以写
//        readOnlyBuffer.position(0);
//        readOnlyBuffer.put((byte)2);
    }
}
  1. 字节缓冲区
    为了便于示例,前面的例子都使用了CharBuffer缓冲区,但实际上应用最广,使用频率最高,也是最重要的缓冲区是字节缓冲区ByteBuffer。因为ByteBuffer中直接存储字节,所以在不同的操作系统、硬件平台、文件系统和JDK之间传递数据时不涉及编码、解码和乱码问题,也不涉及Big-Endian和Little-Endian大小端问题,所以它是使用最为便利的一种缓冲区。

2.1视图缓冲区
ByteBuffer中存储的是字节,有时为了方便,可以使用asCharBuffer()等方法将ByteBuffer转换为存储某基本类型的视图,例如CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer和FloatBuffer。
如此转换后,这两个缓冲区共享同一个内部数组,但是对数组内元素的视角不同。以CharBuffer和ByteBuffer为例,ByteBuffer将其视为一个个的字节(1个字节),而CharBuffer则将其视为一个个的字符(2个字节)。若此ByteBuffer的capacity为12,则对应的CharBuffer的capacity为12/2=6。与duplicate创建的复制缓冲区类似,该CharBuffer和ByteBuffer也各自管理自己的缓冲区属性。
还有一点需要注意的是,在创建视图缓冲区的时候ByteBuffer的position属性的取值很重要,视图会以当前position的值为开头,以limit为结尾。例子如下:

private static void testElementView() {
    ByteBuffer buffer =ByteBuffer.allocate(12);
    //存入四个字节,0x00000042
    buffer.put((byte) 0x00).put((byte)0x00).put((byte) 0x00).put((byte) 0x42);
    buffer.position(0);
    //转换为IntBuffer,并取出一个int(四个字节)
    IntBuffer intBuffer =buffer.asIntBuffer();
    int i =intBuffer.get();
   System.out.println(Integer.toHexString(i));
}

不同元素需要的字节数不同:char为2字节,short为2字节,int为4字节,float为4字节,long为8字节,double也是8字节。

2.2存取数据元素
也可以不通过视图缓冲区,直接向ByteBuffer中存入和取出不同类型的元素,其方法名为putChar()或者getChar()之类。例子如下:

private static void testPutAndGetElement() {
    ByteBuffer buffer =ByteBuffer.allocate(12);
    //直接存入一个int
    buffer.putInt(0x1234abcd);
    //以byte分别取出
    buffer.position(0);
    byte b1 = buffer.get();
    byte b2 = buffer.get();
    byte b3 = buffer.get();
    byte b4 = buffer.get();
   System.out.println(Integer.toHexString(b1&0xff));
   System.out.println(Integer.toHexString(b2&0xff));
   System.out.println(Integer.toHexString(b3&0xff));
   System.out.println(Integer.toHexString(b4&0xff));
}

2.3 字节序

终于又要讲到字节序了,详细参见https://zhuanlan.zhihu.com/p/25435644

简单说来,当某个元素(char、int、double)的长度超过了1个字节时,则由于种种历史原因,它在内存中的存储方式有两种,一种是Big-Endian,一种是Little-Endian。
Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。 简单来说,就是我们人类熟悉的存放方式。
Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
Java默认是使用Big-Endian的,因此上面的代码都是以这种方式来存放元素的。但是,其他的一些硬件(CPU)、操作系统或者语言可能是以Little-Endian的方式来存储元素的。因此NIO提供了相应的API来支持缓冲区设置为不同的字节序,其方法很简单,代码如下:

    privatestatic void testByteOrder() {
        ByteBuffer buffer =ByteBuffer.allocate(12);
        //直接存入一个int
       buffer.putInt(0x1234abcd);
       buffer.position(0);
        intbig_endian= buffer.getInt();
       System.out.println(Integer.toHexString(big_endian));
       buffer.rewind();
        intlittle_endian=buffer.order(ByteOrder.LITTLE_ENDIAN).getInt();
       System.out.println(Integer.toHexString(little_endian));
    }

输出为:
1234abcd
cdab3412
使用order方法可以随时设置buffer的字节序,其参数取值为ByteOrder.LITTLE_ENDIAN以及ByteOrder.BIG_ENDIAN。

2.4直接缓冲区 DirectByteBuffer
最后一个需要掌握的概念是直接缓冲区,它是以创建时的开销换取了IO时的高效率。另外一点是,直接缓冲区使用的内存是直接调用了操作系统api分配的,绕过了JVM堆栈。
直接缓冲区通过ByteBuffer.allocateDirect()方法创建,并可以调用isDirect()来查询一个缓冲区是否为直接缓冲区。
一般来说,直接缓冲区是最好的IO选择。

public class NioTest8 {

    public static void main(String[] args) throws IOException {
        FileInputStream inputStream = new FileInputStream("input2.txt");
        FileOutputStream outputStream = new FileOutputStream("output2.txt");

        FileChannel inputChannel = inputStream.getChannel();
        FileChannel outputChannel = outputStream.getChannel();

        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

        while(true){
            //每一次读取之前都将buffer状态初始化
            buffer.clear();
            int read = inputChannel.read(buffer);

            System.out.println("read:" + read);

            if(-1 == read){
                break;
            }
            buffer.flip();
            outputChannel.write(buffer);
        }

        inputChannel.close();
        outputChannel.close();
    }
}

2.5、MappedByteBuffer 内存映射文件
文件的内容直接映射到内存里面,在内存中任何的信息修改,最终都会被写入到磁盘文件中,即MappedByteBuffer是一种允许java程序直接从内存访问的特殊的文件,可以将整个文件或整个文件的一部分映射到内存中,由操作系统负责将页面请求的内存数据修改写入到文件中。应用程序只需要处理内存的数据,这样可以实现迅速的IO操作,用于内存映射文件的这个内存是堆外内存

public class NioTest9 {

    public static void main(String[] args) throws IOException {
        RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest9.txt","rw");
        FileChannel fileChannel = randomAccessFile.getChannel();

        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 6);

        mappedByteBuffer.put(0, (byte) 'a');
        mappedByteBuffer.put(4, (byte) 'b');

        randomAccessFile.close();
    }
}
  1. 小结
    与Stream相比,Buffer引入了更多的概念和复杂性,这一切的努力都是为了实现NIO的经典编程模式,即用一个线程来控制多路IO,从而极大的提高服务器端IO效率。Buffer、Channel和Selector共同实现了NIO的编程模式,其中Buffer也可以被独立的使用,用来完成缓冲区的功能。

参考:
Java NIO编程实例之一Buffer

Java NIO通俗编程之缓冲区内部细节状态变量position,limit,capacity(二)

Java NIO:Buffer、Channel 和 Selector

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容