NIO学习笔记一之Buffer

参考:http://ifeve.com/buffers/

有一些个人理解,大家辩证地看,有问题的地方,还请大家指出。

Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的。

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

1.Buffer的基本用法

使用Buffer读写数据一般遵循以下几个步骤:

  • 向Buffer写数据
  • 调用filp()方法,该方法的作用是将Buffer从写模式切换到读模式,后面会具体介绍
  • 从Buffer中读取数据
  • 调用clear()方法或是compact()方法,这两个方法的作用主要是清空缓冲区,其实并不是真的清空缓冲区的数据,只是更改了几个标志位,后面会具体介绍

一个栗子:

    ByteBuffer buffer = ByteBuffer.allocate(48);
    buffer.put((byte)4);
    buffer.flip();
    for(int i=0;i<buffer.limit();i++){
        System.out.println(buffer.get());
    }

Buffer的几个重要成员变量

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

capacity

字面意思理解,这个变量指的是容量,我们可以把它理解为Buffer的容量,也就是我们最多可以给Buffer写入capacity大小的数据

position:

position表示当前读取(或是写入)的位置,写入(或是读取)一个数据后,position会指向下一个可读(或是可写)的位置。

limit:

limit表示我们最多可以读到(或写到)limit位置,这个变量容易和capacity混淆。

这个变量出现的原因主要是我们读和写都用的是一块内存,所以需要一个标记,来只是我们最多可以读到(或写到)什么位置。

mark:

这个变量是一个记号,用来记住当前的position,之后介绍mark()方法的时候会具体介绍。

3.Buffer的类型

Buffer主要有以下一种类型,我们主要会以ByteBuffer为主介绍。

4.Buffer的分配

堆中分配:

    ByteBuffer buffer = ByteBuffer.allocate(48);

我们来看一下allocate(int capacity)的代码:

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

我们可以看到,首先做了基本的校验,然后就return了一个HeapByteBuffer的对象。

我们继续看一下HeapByteBuffer的构造方法:

    HeapByteBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new byte[cap], 0);
    }

我们可以看到,该构造方法中直接调用了父类(也就是ByteBuffer类)的构造方法:

    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                 byte[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

我们可以看到:这两个方法都是包访问权限,也就是只有java.nio包中的类可以调用(直白地说,就是我们是不能直接调用的),所以这个allocate(int capacity)是一个工厂方法!

ByteBuffer中调用的是HeapByteBuffer的构造方法,也是就在JVM堆中分配内存,所以我们调用allocate(int capacity)得到的是普通的对象。

直接内存中分配:

我们还可以在直接内存中分配ByteBuffer对象:

    ByteBuffer buffer = ByteBuffer.allocateDirect(48);

我们来看一下是怎么分配的:

    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

再点进去看一下DirectByteBuffer(int cap):

    DirectByteBuffer(int cap) {                   // package-private

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

我们可以看到它是通过调用unsafe的allocateMemory(long size)来分配内存的,我们由unsafe这个名字就可以知道,它是危险的,它使JVM 和操作系统内核可以交流。

unsafe的allocateMemory(long size)是一个本地方法,我们暂时看不到了。

但是,我们可以知道,allocateMemory(long size)分配的是直接内存,也就是JVM堆外内存,而allocate(int capacity)分配的是堆内内存。

为什么要支持分配直接内存呢?这个问题从网络分层来说起吧:

我们的TCP/IP模型,将网络分为了几大层:应用层、传输层、网络层和链路层。其中,传输层(包括传输层)以下是运行在内核空间的,应用层是运行在用户空间的。我们要用通过网络接收数据,要将内核空间的数据复制要用户空间;同样地,要通过网络发送数据,要将用户空间的数据复制到内核空间,再进行发送。

这样复制来复制去效率未免很低,于是我们可以通过直接分配直接内存的方式,来减少一次复制。

但是这个直接内存用起来是有一些注意事项的,后面我会专门写一篇文章来总结这个。

4.几个方法介绍

flip()方法

flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。

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

position为写的数据的位置,表示就写了这么多的数据,我们读当然不能超过它啦,于是就将limit设置为position,我们读要从头开始读,就将position置为0。

rewind()方法

Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。

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

clear()方法

一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()来完成。

clear()方法将position置为0,limit置为capacity,mark置为-1,也就是将一切置为了原始状态。我们可以看到,它并没有真正地清空缓冲区的数据,只是将标志位还原。

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

compact()方法

compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素的后面。

mark()方法

这个方法是与下面的reset()方法配套使用的,这个方法很简单,只是将当前的position记录在mark上

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

reset()方法

这个方法就是将position恢复为之间记录下来的值

    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();//必要的校验
        position = m;
        return this;
    }

猜你喜欢

转载自blog.csdn.net/qq_37043780/article/details/82014489