NIO读书笔记

Posted by arnkore on March 22, 2017

缓冲区(Buffer)

缓冲区基础概念

属性

所有的缓冲区都提供了4个属性来描述其所包含的数据信息:

  • capacity:缓冲区能够容纳数据元素的最大数量,该值在缓冲区创建时被设定,并且不能被改变。
  • limit:0~limit-1 是数据元素的有效索引范围,limit 是现存元素的计数,指明了缓冲区有效内容的末端。
  • position:下一个要被读或写的元素的索引。get() 和 put() 函数会自动更新位置索引。
  • mark:一个备忘位置。初始是未定义的(undefined,实现中一般定义为-1),调用 mark() 来设定 mark = position,调用 reset() 来设定 position = mark。

这四个属性之间总是遵循以下关系:

mark <= position <= limit <= capacity

0 <= position <= limit <= capacity

ByteBuffer 的逻辑视图

缓冲区API

package java.nio;

public abstract class Buffer { 
    public final int capacity();
    public final int position();
    public final Buffer position(int newPosition);
    public final int limit(); 
    public final Buffer limit (int newLimit);
    public final Buffer mark();
    public final Buffer reset();
    public final Buffer clear();
    public final Buffer flip();
    public final Buffer rewind();
    public final int remaining();
    public final boolean hasRemaining();
    public abstract boolean isReadOnly(); 
}

从上面的 API 可以看出 Buffer 是支持级联调用的。

存取&填充

public abstract class ByteBuffer extends Buffer implements Comparable { 
    // This is a partial API listing 
    public abstract byte get(); 
    public abstract byte get(int index); 
    public abstract ByteBuffer put(byte b); 
    public abstract ByteBuffer put(int index, byte b); 
}

get 和 put 可以是相对的或者是绝对的。相对方案是指不带索引参数的函数。
当相对函数被调用时,位置在返回时前进一,而绝对存取不会影响缓冲区的位置索引。

翻转

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

flip() 函数将一个填充状态的缓冲区(能够继续添加数据)翻转为一个释放状态的缓冲区(准备读出数据)。
rewind() 函数与 flip() 类似,但是不影响上界属性(limit),它只是将 position 重新设定为0,同时将 mark 属性丢弃。

释放

缓冲区并不是线程安全的,如果多线程同时存取同一个缓冲区,在存取缓冲区之前需要进行同步。
下面这个例子允许多线程同时从缓冲区释放元素

while (buffer.hasRemaining()) {
    buffer.get();
}

压缩

public abstract class ByteBuffer extends Buffer implements Comparable {
    // This is a partial API listing 
    public abstract ByteBuffer compact();
}

调用 compact 的作用是丢弃已经释放的数据元素,保留未释放的数据元素,并使缓冲区对“继续填充数据”准备就绪。调用 compact 会发生几件事情:

  • 将未被释放的元素,移动到 buffer 的头部。
  • 将 position 置为被复制的数据元素的数目。
  • 上界属性(limit)被设为容量的值。

例如,初始 buffer 如下:

调用 buffer.compact() 之后

这里发生了几件事。数据元素 2-5 被复制到 0-3 位置。位置 4 和 5 不受影响,正在或已经超出了当前位置(position),因此是“死的”。它们可以被之后的 put() 调用重写。

标记

标记,使缓冲区能够记住当前位置(position)的值并在之后返回(reset)。缓冲区标记在 mark() 函数被调用前是未定义的,调用时被设定为当前位置的值。reset() 函数将位置(position)设为标记值。如果标记值未定义,调用 reset() 将抛出 InvalidMarkException。flip()、rewind()、clear() 将抛弃已经设定的标记,将其置为未定义状态。

比较

public abstract class ByteBuffer extends Buffer implements Comparable { 
    // This is a partial API listing 
    public boolean equals(Object ob);
    public int compareTo(Object ob);
}

两个缓冲区被认为相等的充要条件是:

  1. 两个缓冲区的类型相同,缓冲区包含的数据元素的类型相同,包含不同数据类型的缓冲区永远不会相等,而且缓冲区对象绝不会等于非缓冲区对象。
  2. 两个缓冲区都剩余同样数量的元素。缓冲区的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但缓冲区中剩余元素的数目必须相同(从位置到 limit)。
  3. 每个缓冲区应被 get() 函数返回的剩余数据元素序列必须相同。

批量移动

public abstract class CharBuffer extends Buffer implements CharSequence, Comparable {
    // This is a partial API listing 
    public CharBuffer get(char [] dst);
    public CharBuffer get(char [] dst, int offset, int length);
    public final CharBuffer put(char[] src);
    public CharBuffer put(char [] src, int offset, int length);
    public CharBuffer put(CharBuffer src);
    public final CharBuffer put(String src);
    public CharBuffer put(String src, int start, int end) 
}

批量复制版本的缓冲区能够利用本地代码或其它优化手段来高校传输数据。

对于批量版本的 get():如果缓冲区中的数据不够完全填满数组,那么不会有数据被传递,缓冲区的状态保持不变,同时抛出 BufferUnderflowException。

对于批量版本的 put():如果缓冲区有足够的空间来容纳数组中的数据(buffer.remaining() > array.length),数据将会被复制到从当前位置开始的缓冲区,同时位置(position)需要增加复制的元素数目。如果缓冲区没有足够的空间来容纳数组中的数据,那么不会有数据被传递,同时抛出一个 BufferOverflowException。

对于两个缓冲区之间的数据移动,dstBuffer.put(srcBuffer):如果 srcBuffer.remaing() > dstBuffer.remaining(),那么不会有数据被移动,同时抛出 BufferOverflowException。


文献引用
[1]. 《JAVA NIO》中文版