图片 3

[翻译] java NIO Buffer

本篇主要讲解如何使用直接内存(堆外内存),并按照下面的步骤进行说明:

原文地址:

相关背景-->读写操作-->关键属性-->读写实践-->扩展-->参考说明

JAVA NIO
是在和channel交互的时候使用的。正如你所知道的,数据是从channel中读入到buffer,从buffer中写入到channel中的。

希望对想使用直接内存的朋友,提供点快捷的参考。

buffer本质上是一块你可以写入然后读出的一块内存。这个内存块被封装在NIO的buffer对象中,它提供了一系列方法,使得我们可以很轻松的操作这个内存块。

数据类型

下面这些,都是在使用DirectBuffer中必备的一些常识,暂作了解吧!如果想要深入理解,可以看看下面参考的那些博客。

 

基本类型长度

在Java中有很多的基本类型,比如:

  • byte,一个字节是8位bit,也就是1B
  • short,16位bit,也就是2B
  • int,32位bit,也就是4B
  • long, 64位bit,也就是8B
  • char,16位bit,也就是2B
  • float,32位bit,也就是4B
  • double,64位bit,也就是8B

不同的类型都会按照自己的位数来存储,并且可以自动进行转换提升。
bytecharshort都可以自动提升为int,如果操作数有long,就会自动提升为longfloatdouble也是如此。

Buffer基本的使用

大端小端

由于一个数据类型可能有很多个字节组成的,那么它们是如何摆放的。这个是有讲究的:

  • 大端:低地址位 存放 高有效字节
  • 小端:低地址位 存放 低有效字节

举个例子,一个char是有两个字节组成的,这两个字节存储可能会显示成如下的模样,比如字符a:

              低地址位    高地址位
大端;        00              96
小端:        96              00

  使用buffer读写数据至少有一下四步:

String与new String的区别

再说说"hello"new String("hello")的区别:

如果是"hello",JVM会先去共享的字符串池中查找,有没有"hello"这个词,如果有直接返回它的引用;如果没有,就会创建这个对象,再返回。因此,"a"+"b"相当于存在3个对象,分别是"a""b""ab"

new String("hello"),则省去了查找的过程,直接就创建一个hello的对象,并且返回引用。

  1.   写入数据到buffer中。
  2.   调用buffer.flip()。
  3.   从buffer中读出数据。
  4.   调用buffer.clear()或者buffer.compact()。

读写数据

在直接内存中,通过allocateDirect(int byte_length)申请直接内存。这段内存可以理解为一段普通的基于Byte的数组,因此插入和读取都跟普通的数组差不多。

只不过提供了基于不同数据类型的插入方法,比如:

  • put(byte) 插入一个byte
  • put(byte[]) 插入一个byte数组
  • putChar(char) 插入字符
  • putInt(int) 插入Int
  • putLong(long) 插入long

等等….详细的使用方法,也可以参考下面的图片:

图片 1

对应读取数据,跟写入差不多:

图片 2

注意所有没有index参数的方法,都是按照当前position的位置进行操作的。

下面看看什么是position,还有什么其他的属性吧!

  当你写入数据的时候,buffer会跟踪你写入了多少数据。当你需要读取数据的时候,你可以通过调用flip()方法将写入模式转换为读取模式。在读取模式中,你可以从buffer中读取所有写入的数据。

基本的属性值

它有几个关键的指标:

mark-->position-->limit-->capacity

另外,还有remaining=limit-position

先说说他们的意思吧!

  当你读取完所有的数据的时候,你需要清除buffer中的数据,使得buffer可以再次被写入。有两种方法可以清除:调用clear()或者compact()。clear()方法清除整个buffer,而compact()方法只会清除已经读取的数据,未读取的数据则会移动到buffer的开始处,然后在未读数据的后面再开始写入。

当前位置——position

position是当前数组的指针,指示当前数据位置。举个例子:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
buffer.putChar('a');
System.out.println(buffer);
buffer.putChar('c');
System.out.println(buffer);
buffer.putInt(10);
System.out.println(buffer);

由于一个char是2个字节,一个Int是4个字节,因此position的位置分别是:

2,4,8

注意,Position的位置是插入数据的当前位置,如果插入数据,就会自动后移。
也就是说,如果存储的是两个字节的数据,position的位置是在第三个字节上,下标就是2。

java.nio.DirectByteBuffer[pos=2 lim=1024 cap=1024]
java.nio.DirectByteBuffer[pos=4 lim=1024 cap=1024]
java.nio.DirectByteBuffer[pos=8 lim=1024 cap=1024]

position可以通过position()获得,也可以通过position(int)设置。

//position(int)方法的源码
public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        if (mark > position) mark = -1;
        return this;
    }

注意:position的位置要比limit小,比mark大

  下面是一个简单的buffer的例子,写入,转换模式,读取,清除。

空间容量——capacity

capacity是当前申请的直接内存的容量,它是申请后就不会改变的。

capacity则可以通过capacity()方法获得。

 1 RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
 2 FileChannel inChannel = aFile.getChannel();
 3 
 4 //create buffer with capacity of 48 bytes
 5 ByteBuffer buf = ByteBuffer.allocate(48);
 6 
 7 int bytesRead = inChannel.read(buf); //read into buffer.
 8 while (bytesRead != -1) {
 9 
10   buf.flip();  //make buffer ready for read
11 
12   while(buf.hasRemaining()){
13       System.out.print((char) buf.get()); // read 1 byte at a time
14   }
15 
16   buf.clear(); //make buffer ready for writing
17   bytesRead = inChannel.read(buf);
18 }
19 aFile.close();

限制大小——limit

我们可能想要改变这段直接内存的大小,因此可以通过一个叫做Limit的属性设置。

limit则可以通过limit()获得,通过limit(int)进行设置。

注意limit要比mark和position大,比capacity小。

//limit(int)方法的源码
public final Buffer limit(int newLimit) {
        if ((newLimit > capacity) || (newLimit < 0))
            throw new IllegalArgumentException();
        limit = newLimit;
        if (position > limit) position = limit;
        if (mark > limit) mark = -1;
        return this;
    }

 

标记位置——mark

mark,就是一个标记为而已,记录当前的position的值。常用的场景,就是记录某一次插入数据的位置,方便下一次进行回溯。

  • 可以使用mark()方法进行标记,
  • 使用reset()方法进行清除,
  • 使用rewind()方法进行初始化

    //mark方法标记当前的position,默认为-1
    public final Buffer mark() {
    mark = position;
    return this;
    }
    //reset方法重置mark的位置,position的位置,不能小于mark的位置,否则会出错
    public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
    }
    //重置mark为-1.position为0
    public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
    }
    

    使用案例

    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    buffer.putChar('a');
    buffer.putChar('c');
    System.out.println("插入完数据 " + buffer);
    buffer.mark();// 记录mark的位置
    buffer.position(30);// 设置的position一定要比mark大,否则mark无法重置
    System.out.println("reset前 " + buffer);
    buffer.reset();// 重置reset ,reset后的position=mark
    System.out.println("reset后 " + buffer);
    buffer.rewind();//清除标记,position变成0,mark变成-1
    System.out.println("清除标记后 " + buffer);
    

    可以看到如下的运行结果:

    插入完数据 java.nio.DirectByteBuffer[pos=4 lim=1024 cap=1024]
    reset前 java.nio.DirectByteBuffer[pos=30 lim=1024 cap=1024]
    reset后 java.nio.DirectByteBuffer[pos=4 lim=1024 cap=1024]
    清除标记后 java.nio.DirectByteBuffer[pos=0 lim=1024 cap=1024]
    

Buffer Capacity,Position,Limit

剩余空间——remaing

remaing则表示当前的剩余空间:

public final int remaining() {
        return limit - position;
}

  JAVA NIO
是在和channel交互的时候使用的。正如你所知道的,数据是从channel中读入到buffer,从buffer中写入到channel中的。

读写实践

写操作主要就是按照自己的数据类型,写入到直接内存中,注意每次写入数据的时候,position都会自动加上写入数据的长度,指向下一个该写入的起始位置:

下面看看如何写入一段byte[]或者字符串:

ByteBuffer buffer = ByteBuffer.allocateDirect(10);
byte[] data = {1,2};
buffer.put(data);
System.out.println("写byte[]后 " + buffer);
buffer.clear();
buffer.put("hello".getBytes());
System.out.println("写string后 " + buffer);

输出的内容为:

写byte[]后 java.nio.DirectByteBuffer[pos=2 lim=10 cap=10]
写string后 java.nio.DirectByteBuffer[pos=5 lim=10 cap=10]

读的时候,可以通过一个外部的byte[]数组进行读取。由于没有找到直接操作直接内存的方法:
因此如果想在JVM应用中使用直接内存,需要申请一段堆中的空间,存放数据。

如果有更好的方法,还请留言。

ByteBuffer buffer = ByteBuffer.allocateDirect(10);
buffer.put(new byte[]{1,2,3,4});
System.out.println("刚写完数据 " +buffer);
buffer.flip();
System.out.println("flip之后 " +buffer);
byte[] target = new byte[buffer.limit()];
buffer.get(target);//自动读取target.length个数据
for(byte b : target){
    System.out.println(b);
}
System.out.println("读取完数组 " +buffer);

输出为

刚写完数据 java.nio.DirectByteBuffer[pos=4 lim=10 cap=10]
flip之后 java.nio.DirectByteBuffer[pos=0 lim=4 cap=10]
1
2
3
4
读取完数组 java.nio.DirectByteBuffer[pos=4 lim=4 cap=10]

buffer本质上是一块你可以写入然后读出的一块内存。这个内存块被封装在NIO的buffer对象中,它提供了一系列方法,使得我们可以很轻松的操作这个内存块。

常用方法

上面的读写例子中,有几个常用的方法:

  为了理解buffer是如何工作的,有三个buffer的属性你必须熟悉。

clear()

这个方法用于清除mark和position,还有limit的位置:

public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
  • capacity
  • position
  • limit

flip()

这个方法主要用于改变当前的Position为limit,主要是用于读取操作。

public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
}

  position和limit的意义依赖于buffer的读写模式,而capacity则不管什么模式都是一样的。下图可以解释position,limit,capacity的区别。

compact()

这个方法在读取一部分数据的时候比较常用。

它会把当前的Position移到0,然后position+1移到1。

public ByteBuffer compact() {
        int pos = position();
        int lim = limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);

        unsafe.copyMemory(ix(pos), ix(0), rem << 0);
        position(rem);
        limit(capacity());
        discardMark();
        return this;
    }

比如一段空间内容为:

123456789

当position的位置在2时,调用compact方法,会变成:

345678989

图片 3

isDirect()

这个方法用于判断是否是直接内存。如果是返回true,如果不是返回false。

Capacity

rewind()

这个方法用于重置mark标记:

public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

  作为一个内存块,buffer有一个固定的大小,同样也叫“capacity”。你只能写入capacity大小的bytes,long,chars等到buffer中。一旦buffer被写满了,在你写入更多的数据之前你需要清空它(读取或者clear)。

参考

1 Java基本数据类型
2 Java中大端与小端

Position

  当你写入数据的时候,你同时也知道一个确切的位置。初始的时候,position的值是0,当一个byte,long等写入buffer的时候,position将会指向下一个待写入的空间。position的最大值是capacity
- 1.

  当你读取数据的时候,同样是从一个给定的位置开始的。当你将buffer从写入模式转换到读取模式的时候,position重置为0,你从position的位置读取数据,然后position指向下一个读取的空间。

Limit

  在写模式下,limit表示你最多可以向buffer中写入多少数据。写模式下limit等于capacity。

  当把buffer转换到读模式下时,limit意味着你可以读取多少数据。因此当将buffer转换到读模式的时候,limit等于写模式下的写入到的position。换句话说,你写入多少数据就可以读取多少数据。

Buffer types

Java NIO 实现了一下的一些Buffer类型。

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

  正如你所知,java nio
的类型代表这不同的数据类型。换句话说,你可以通过使用char,short,int,long,float
或 double类型来操作缓冲区中的字节。

  MappedByteBuffer有一点特殊,我们将会在它所在的章节进行介绍。

分配Buffer

  在得到一个buffer之前,首先要为他分配空间。每一个buffer的类都有一个
allocate()的方法可以做这件事。下面一个例子表示给一个ByteBuffer分配一个48字节的空间。

ByteBuffer buf = ByteBuffer.allocate(48);

下面一个例子时表示给一个CharBuffer分配一个1024字符的空间。

CharBuffer buf = CharBuffer.allocate(1024);

向buffer中写入数据

  有两种方法可以向buffer中写入数据:

  1.   从channel写入到buffer
  2.   通过使用buffer的put()方法

下面一个例子表示从channel写入到buffer。

int bytesRead = inChannel.read(buf); //read into buffer.

下面一个例子表示使用buffer的put()函数写入数据

buf.put(127);  

  put()函数的重载的方法有很多个,因此有多种写入buffer的方式。例如:写入到一个具体的位置,或者是写入一个字节数组。更多详情请参照JDK
doc。

flip()

  flip()函数是将buffer的模式从写转换为读,调用flip()使得position的值设置为0,limit的值设置为刚刚position的值。

  换句话说,position现在标志读的位置,limit标志有多少bytes,chars等写入到了buffer中,即有多少可读。

从Buffer中读数据

  从buffer中读数据也有两种方式:

  1、从buffer中读数据到channel中。

  2、使用buffer的get()函数。

  下面是一个从buffer读数据到写入到channel的例子。

int bytesWritten = inChannel.write(buf);

  下面是buffer用自身的get()函数的例子。

byte aByte = buf.get();    

  get()函数的重载的方法有很多个,因此有多种读取buffer数据的方式。例如:从一个具体的位置读取数据,或者是读取数据到一个字节数组。更多详情请参照JDK
doc。

rewind ()

  rewind()函数将position设置为0,因此你可以重读刚刚读取的数据。limit仍然不变,以此仍然可以标志有多少读。

clear() 和 compact()

  一旦你从buffer中读完了数据就要将buffer重新标记为可写。可以使用clear()或者compact()来完成。

  如果使用clear(),那么position重新设置为0,limit设置为capacity。换句话说buffer被清除了。在buffer中的数据没有被清除。只有标记才能告诉你buffer中哪里可以写。

  如果有未读的数据,调用clear()的话buffer将会把他们忽略。意味着不管有多少标记告诉你还有数据未读,你也不可能再读取这些数据了。

  如果仍然有些数据未读,而且你现在必须做一些写的操作,那么就调用compact()方法而不是clear()。

  compact()会复制所有未读的数据到buffer的开始处。然后它将position设置为最后一个未读元素的右边的第一个位置。limit则和clear()的操作一致,仍然设置为capacity。现在buffer已经可以写了,而且你也不会覆盖为读的数据。

mark()和reset()

  你可以调用mark()方法标记一个位置,然后你可以调用reset()方法返回刚刚你标记的位置。下面是一个简单的例子。

buffer.mark();

//call buffer.get() a couple of times, e.g. during parsing.

buffer.reset();  //set position back to mark.   

equals() 和 compareTo()

  可以使用equals() 和compareTo()来比较两个buffer的大小。

 equals()

  两个buffer相等的条件

  1.     相同的类型(byte,char等)
  2.     buffer中有相同的剩余空间。
  3.     所有剩余的bytes,chars等相等。

  正如你所见到的,equals()只是比较buffer的一部分,而不是他的全部。事实上,它只是比较buffer中剩余的部分。

 compareTo()

  compareTo()方法比较两个buffer中剩余的元素。一个buffer比另一个buffer小的条件是:

  第一个元素比它在另一个buffer中的相同位置的值小。

  第一个buffer比第二个buffer的长度小,并且第一个buffer中的元素与之对应的第二个buffer的元素都相等。

发表评论

电子邮件地址不会被公开。 必填项已用*标注