Netty专栏(3)--好好聊聊Java NIO的Buffer

200 阅读4分钟

NIO的Buffer类

NIO的Buffer(缓冲区)本质上是一个内存块,既可以写入数据,也可以从中读取数据。 Java NIO中代表缓冲区的Buffer类是一个抽象类,位于java.nio包中。 NIO的Buffer的内部是一个内存块(数组),此类与普通的内存块(Java数组)不同的 是:NIO Buffer对象,提供了一组比较有效的方法,用来进行写入和读取的交替访问。

截屏2023-05-06 20.51.12.png

package java.nio;

import jdk.internal.HotSpotIntrinsicCandidate;
import jdk.internal.access.JavaNioAccess;
import jdk.internal.access.SharedSecrets;
import jdk.internal.misc.Unsafe;

import java.util.Spliterator;

public abstract class Buffer {
    // Cached unsafe-access object
    static final Unsafe UNSAFE = Unsafe.getUnsafe();

    /**
     * The characteristics of Spliterators that traverse and split elements
     * maintained in Buffers.
     */
    static final int SPLITERATOR_CHARACTERISTICS =
        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
    //重要属性
    private int mark = -1;  //读写位置的临时备份
    private int position = 0;  //读写位置
    private int limit;  //读写的限制
    private int capacity;   //读写位置的临时备份
    long address;

    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw createCapacityException(cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }

    static IllegalArgumentException createSameBufferException() {
        return new IllegalArgumentException("The source buffer is this buffer");
    }

    static IllegalArgumentException createCapacityException(int capacity) {
        assert capacity < 0 : "capacity expected to be negative";
        return new IllegalArgumentException("capacity < 0: ("
            + capacity + " < 0)");
    }

    public final int capacity() {
        return capacity;
    }

    public final int position() {
        return position;
    }

    public Buffer position(int newPosition) {
        if (newPosition > limit | newPosition < 0)
            throw createPositionException(newPosition);
        if (mark > newPosition) mark = -1;
        position = newPosition;
        return this;
    }

    private IllegalArgumentException createPositionException(int newPosition) {
        String msg = null;

        if (newPosition > limit) {
            msg = "newPosition > limit: (" + newPosition + " > " + limit + ")";
        } else { // assume negative
            assert newPosition < 0 : "newPosition expected to be negative";
            msg = "newPosition < 0: (" + newPosition + " < 0)";
        }

        return new IllegalArgumentException(msg);
    }

    public final int limit() {
        return limit;
    }

    public Buffer limit(int newLimit) {
        if (newLimit > capacity | newLimit < 0)
            throw createLimitException(newLimit);
        limit = newLimit;
        if (position > newLimit) position = newLimit;
        if (mark > newLimit) mark = -1;
        return this;
    }

    private IllegalArgumentException createLimitException(int newLimit) {
        String msg = null;

        if (newLimit > capacity) {
            msg = "newLimit > capacity: (" + newLimit + " > " + capacity + ")";
        } else { // assume negative
            assert newLimit < 0 : "newLimit expected to be negative";
            msg = "newLimit < 0: (" + newLimit + " < 0)";
        }

        return new IllegalArgumentException(msg);
    }

    public Buffer mark() {
        mark = position;
        return this;
    }

    public Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }
    public Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

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

   
    public Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
    public final int remaining() {
        int rem = limit - position;
        return rem > 0 ? rem : 0;
    }

    public final boolean hasRemaining() {
        return position < limit;
    }

    public abstract boolean isReadOnly();

  
    public abstract boolean hasArray();

    public abstract Object array();

 
    public abstract int arrayOffset();

  
    public abstract boolean isDirect(

    public abstract Buffer slice();

  
    public abstract Buffer duplicate();

    // -- Package-private methods for bounds checking, etc. --

 
    abstract Object base();

    final int nextGetIndex() {                          // package-private
        int p = position;
        if (p >= limit)
            throw new BufferUnderflowException();
        position = p + 1;
        return p;
    }

    final int nextGetIndex(int nb) {                    // package-private
        int p = position;
        if (limit - p < nb)
            throw new BufferUnderflowException();
        position = p + nb;
        return p;
    }

    final int nextPutIndex() {                          // package-private
        int p = position;
        if (p >= limit)
            throw new BufferOverflowException();
        position = p + 1;
        return p;
    }

    final int nextPutIndex(int nb) {                    // package-private
        int p = position;
        if (limit - p < nb)
            throw new BufferOverflowException();
        position = p + nb;
        return p;
    }

    @HotSpotIntrinsicCandidate
    final int checkIndex(int i) {                       // package-private
        if ((i < 0) || (i >= limit))
            throw new IndexOutOfBoundsException();
        return i;
    }

    final int checkIndex(int i, int nb) {               // package-private
        if ((i < 0) || (nb > limit - i))
            throw new IndexOutOfBoundsException();
        return i;
    }

    final int markValue() {                             // package-private
        return mark;
    }

    final void truncate() {                             // package-private
        mark = -1;
        position = 0;
        limit = 0;
        capacity = 0;
    }

    final void discardMark() {                          // package-private
        mark = -1;
    }

    static void checkBounds(int off, int len, int size) { // package-private
        if ((off | len | (off + len) | (size - (off + len))) < 0)
            throw new IndexOutOfBoundsException();
    }

    static {
        // setup access to this package in SharedSecrets
        SharedSecrets.setJavaNioAccess(
            new JavaNioAccess() {
                @Override
                public JavaNioAccess.BufferPool getDirectBufferPool() {
                    return Bits.BUFFER_POOL;
                }
                @Override
                public ByteBuffer newDirectByteBuffer(long addr, int cap, Object ob) {
                    return new DirectByteBuffer(addr, cap, ob);
                }
                @Override
                public void truncate(Buffer buf) {
                    buf.truncate();
                }
            });
    }

}

NIO Buffer的四个属性

    //重要属性
    private int mark = -1;  //读写位置的临时备份
    private int position = 0;  //读写位置
    private int limit;  //读写的限制
    private int capacity;   //读写位置的临时备份

截屏2023-05-06 20.52.50.png

  1. capacity表示内部容量的大小。

    1. 写入的对象数量超过了capacity容量,缓冲区就满了,不能再写入了。
    2. 初始化后不能再更改,初始化时,会按照capacity分配内部数组的内存,在数组内存分配好之后,它的大小就不能改变。
  2. position表示当前的位置

    position属性的值与缓冲区的读写模式有关。在缓冲区进行读写的模式改变时,position值会进行相应的调整。

    读/写模式下规则:

    1. 在刚进入到读取/写入模式时,position值为0,表示当前的读取/写入位置为从头开始。
    2. 每当一个数据读取/写入到缓冲区之后,position会向后移动到下一个可读取/写入的位置。
    3. 初始的position值为0,最大可读取/写入值为limit–1。当position值达到limit时,缓冲区就已经无空间可读取/写入了。

    写入模式到读取模式的flip翻转过程中,position和limit属性值会进行调整,具体的 规则是:

    1. limit属性被设置成写入模式时的position值,表示可以读取的最大数据位置;
    2. position由原来的写入位置,变成新的可读位置,也就是0,表示可以从头开始读;
    3. 在从缓冲区中读取数据完毕后,limit的值仍然保持在我们调用flip()方法时的值,调用clear()方法能够把所有的状态变化设置为初始化时的值。
  3. limit属性

    limit属性表示可以写入或者读取的最大上限

  4. mark属性

    mark属性的主要作用就是为读位置或者写位置的索引做一个备份。在缓冲区操作(读或者写)的过程当中,可以将当前的position的值,临时存入mark属性中;需要恢复的时候,可以再从mark中取出之前的值,恢复到position属性中,然后,后续可以重新从position位置开始处理(读取或者写入)。

四个属性就介绍完了,看起来可能会有一点抽象,其实很好理解。

下面画图描述一下整个过程:

position、limit、capacity三个属性值之间有一些相对大小的关系:

0 <= position <= limit <= capacity

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

截屏2023-05-06 20.53.10.png

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

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

  1. 把limit设置为当前的position值
  2. 把position设置为0

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

截屏2023-05-06 20.53.29.png

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

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

截屏2023-05-06 20.53.48.png 最后我们用一段代码来验证这个过程,如下所示:

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class Program {

	public static void main(String args[]) throws Exception {
	
		FileInputStream fin = new FileInputStream("d:\\test.txt");
		FileChannel fc = fin.getChannel();
		ByteBuffer buffer = ByteBuffer.allocate(10);
		output("初始化", buffer);

		fc.read(buffer);
		output("调用read()", buffer);

		buffer.flip();
		output("调用flip()", buffer);

		while (buffer.remaining() > 0) {
			byte b = buffer.get();
			// System.out.print(((char)b));
		}
		output("调用get()", buffer);
		
		buffer.clear();
		output("调用clear()", buffer);
		
		fin.close();
	
	}

	public static void output(String step, Buffer buffer) {
	
		System.out.println(step + " : ");
		System.out.print("capacity: " + buffer.capacity() + ", ");
		System.out.print("position: " + buffer.position() + ", ");
		System.out.println("limit: " + buffer.limit());
		System.out.println();
	
	}

}

了解了这个过程那么,我们再来看NIO Buffer类的重要方法。

NIOBuffer类有哪些方法?

  • allocate()创建缓冲区

在使用Buffer(缓冲区)实例之前,我们首先需要获取Buffer子类的实例对象,并且分 配内存空间。如果需要获取一个Buffer实例对象,并不是使用子类的构造器来创建一个实例 对象,而是调用子类的allocate()方法。

看下源码(ByteBuffer子类的):

public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw createCapacityException(capacity);
				//申请堆内存创建缓冲区
        return new HeapByteBuffer(capacity, capacity);
    }
//申请直接内存的方式创建缓冲区
public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
  • put()写入缓冲区

在调用allocate方法分配内存、返回了实例对象后,缓冲区实例对象处于写模式,可以 写入对象,而如果要写入对象到缓冲区,需要调用put方法。put方法很简单,只有一个参数, 即为所需要写入的对象。只不过,写入的数据类型要求与缓冲区的类型保持一致。

//抽象类,由具体缓冲区类进行操作
public abstract ByteBuffer put(byte b);

可以看到这里有四个缓冲区的实现类具体put方法在这些类调用

截屏2023-05-06 20.53.59.png

  • flip()翻转

flip()翻转方法是Buffer类提供的一个模式转变的重要方法,它的作用就是将写入模式翻转成读取模式。看一下源码实现,很简单,就是把position的值给limit,position设置为0.mark为-1.

public Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
  • get()从缓冲区读取

读取数据的方法很简单,可以调用get方法每次从position的位置读取一个数据,并且进 行相应的缓冲区属性的调整。跟put类似,是抽象类具体实现在DirectByteBuffer或HeapByteBuffer里。

// -- Singleton get/put methods --

    /**
     * Relative <i>get</i> method.  Reads the byte at this buffer's
     * current position, and then increments the position.
     *
     * @return  The byte at the buffer's current position
     *
     * @throws  BufferUnderflowException
     *          If the buffer's current position is not smaller than its limit
     */
    public abstract byte get();

截屏2023-05-06 20.54.09.png

  • rewind()倒带

已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带,就像 播放磁带一样倒回去,再重新播放。

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

rewind ()方法,主要是调整了缓冲区的position属性与mark标记属性,具体的调整规则如 下: (1)position重置为0,所以可以重读缓冲区中的所有数据; (2)limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取的元素数量; (3)mark标记被清理,表示之前的临时位置不能再用了。

  • mark( )和reset( )

mark( )和reset( )两个方法是成套使用的:Buffer.mark()方法将当前position的值保存起来, 放在mark属性中,让mark属性记住这个临时位置;之后,可以调用Buffer.reset()方法将mark 的值恢复到position中。

/**
     * Sets this buffer's mark at its position.
     *
     * @return  This buffer
     */
    public Buffer mark() {
        mark = position;
        return this;
    }

    /**
     * Resets this buffer's position to the previously-marked position.
     *
     * <p> Invoking this method neither changes nor discards the mark's
     * value. </p>
     *
     * @return  This buffer
     *
     * @throws  InvalidMarkException
     *          If the mark has not been set
     */
    public Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }
  • clear( )清空缓冲区

在读取模式下,调用clear()方法将缓冲区切换为写入模式。此方法的作用: (1)会将position清零; (2)limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。

/**
     * Clears this buffer.  The position is set to zero, the limit is set to
     * the capacity, and the mark is discarded.
     *
     * <p> Invoke this method before using a sequence of channel-read or
     * <i>put</i> operations to fill this buffer.  For example:
     *
     * <blockquote><pre>
     * buf.clear();     // Prepare buffer for reading
     * in.read(buf);    // Read data</pre></blockquote>
     *
     * <p> This method does not actually erase the data in the buffer, but it
     * is named as if it did because it will most often be used in situations
     * in which that might as well be the case. </p>
     *
     * @return  This buffer
     */
    public Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

最后总结一下Buffer类的使用流程

(1)使用创建子类实例对象的allocate( )方法,创建一个Buffer类的实例对象。 (2)调用put( )方法,将数据写入到缓冲区中。 (3)写入完成后,在开始读取数据前,调用Buffer.flip( )方法,将缓冲区转换为读模式。 (4)调用get( )方法,可以从缓冲区中读取数据。 (5)读取完成后,调用Buffer.clear( )方法或Buffer.compact()方法,将缓冲区转换为写入 模式,可以继续写入。