从入门到放弃Java并发编程NIOBuffer

9次阅读

共计 3778 个字符,预计需要花费 10 分钟才能阅读完成。

前言

上篇【从入门到放弃 -Java】并发编程 -NIO-Channel 中我们学习到 channel 是双向通道,数据通过 channel 在实体(文件、socket)和缓冲区(buffer)中可以双向传输。

本文我们就来学习下 buffer

简介

buffer 即缓冲区,实际上是一块内存,可以用来写入、读取数据。是一个线性的、大小有限的、顺序承载基础数据类型的内存块。

buffer 有三个重要的属性:

  • capacity:缓冲池大小,是不可变的。当 buffer 写满时,需要先清空才能继续写入。
  • limit:是 buffer 中不可以被读或者写的第一个元素的位置,limit 的大小永远不会超过 capacity(在写模式下,limit 等于 capacity)
  • position:是 buffer 中可以被读或者写的第一个元素的位置,position 的大小永远不会超过 limit

除了 boolean 外,每一个基础数据类型都有对应的 buffer。如:ByteBuffer、CharBuffer、LongBuffer 等

buffer 不是线程安全的,如果要在多线程中使用 需要加锁控制

接下来以 ByteBuffer 为例开始学习。

ByteBuffer

allocateDirect

public static ByteBuffer allocateDirect(int capacity) {
    // 会创建一个容量大小为 capacity 的 DirectByteBuffer(ByteBuffer 的子类)return new DirectByteBuffer(capacity);
}

allocate

public static ByteBuffer allocate(int capacity) {if (capacity < 0)
        throw createCapacityException(capacity);
    // 会创建一个容量大小为 capacity 的 HeapByteBuffer(ByteBuffer 的子类)return new HeapByteBuffer(capacity, capacity);
}

HeapByteBuffer 和 DirectByteBuffer 的区别:

  • DirectByteBuffer 是直接调用 native 方法在本机 os::malloc() 创建堆外内存;HeapByteBuffer 是直接在 jvm 的堆中分配内存。
  • 当 buffer 中的数据和磁盘、网络等的交互都在操作系统的内核中发生时,使用 DirectByteBuffer 能避免从内核态 -> 用户态 -> 内核态的切换开销,所有的处理都在内核中进行,性能会比较好
  • 当频繁创建操作数据量比较小的 buffer 时,使用 HeapByteBuffer 在 jvm 堆中分配内存能抵消掉使用 DirectByteBuffer 带来的好处。

wrap

public static ByteBuffer wrap(byte[] array,
                                    int offset, int length)
{
    try {return new HeapByteBuffer(array, offset, length);
    } catch (IllegalArgumentException x) {throw new IndexOutOfBoundsException();
    }
}

public static ByteBuffer wrap(byte[] array) {return wrap(array, 0, array.length);
    }

将 byte 数组包装成一个 ByteBuffer

读数据

  • 使用 get 方法从 Buffer 中读取数据
  • 从 Buffer 中读取数据到 Channel 即:Channel::write() ( 从 buffer 中读取数据写入到资源中,所以是 write)

写数据

  • 使用 put 方法直接设置 Buffer 中的数据
  • 从 Channel 中读取数据到 Buffer 即:Channel::read() ( 从资源中读取数据写入到 buffer 中,所以是 read)

position

// 获取 buffer 中当前 position 的位置
public final int position() {return position;}

// 设置 buffer 的 position 为 newPosition,注意 newPosition 要大于 0 且小于 limit,如果 remark 大于 newPosition 则设置为 -1
public Buffer position(int newPosition) {if (newPosition > limit | newPosition < 0)
         throw createPositionException(newPosition);
     position = newPosition;
     if (mark > position) mark = -1;
     return this;
}

limit

// 获取 buffer 中当前 limit 的位置
public final int limit() {return limit;}

// 设置 buffer 的 limit 为 newLimit,注意 newLimit 要大于 0 且小于 capacity。如果 position 大于 newLimit 这设置为 newLimit,如果 remark 大于 newLimit 则设置为 -1
public Buffer limit(int newLimit) {if (newLimit > capacity | newLimit < 0)
        throw createLimitException(newLimit);
    limit = newLimit;
    if (position > limit) position = limit;
    if (mark > limit) mark = -1;
    return this;
}

mark

public Buffer mark() {
    // 标记 mark 为当前 position
    mark = position;
    return this;
}

将当前位置做标记,在使用 reset 方法时,可以回到当前 mark 的位置

reset

public Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    // 设置 position 为当前 mark
    position = m;
    return this;
}

回到之前设置 mark 的位置

clear

public Buffer clear() {
    // 设置 position 为 0
    position = 0;
    //limit 设置为 capacity 大小
    limit = capacity;
    //mark 设置为 -1(初始化)mark = -1;
    return this;
}

读取完数据后调用 clear,即将 buffer 逻辑上清空了,可以从 0 开始写入数据

flip

public Buffer flip() {
    //limit 设置为当前位置
    limit = position;
    //position 设置为 0
    position = 0;
    //mark 设置为 -1(初始化)mark = -1;
    return this;
}

将 buffer 从写模式设置为读模式,limit 设置为当前 position 的位置,即只能读取 limit 大小的数据

rewind

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

将 position 设置为 0,即从头开始读取

remaining

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

返回 buffer 中还有多少 byte 是未读的

hasRemaining

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

是否已读完

compact

public ByteBuffer compact() {System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
    position(remaining());
    limit(capacity());
    discardMark();
    return this;
}

将 position 和 limit 直接的数据 copy 到 byteBuffer 的起始处,将已读数据清空,并将新的 position 设置为当前未读数据的末尾。这样能避免 clear 方法会将未读数据也清空的问题

slice

public ByteBuffer slice() {
    return new HeapByteBufferR(hb,
                                    -1,
                                    0,
                                    this.remaining(),
                                    this.remaining(),
                                    this.position() + offset);
}

ByteBuffer slice(int pos, int lim) {assert (pos >= 0);
    assert (pos <= lim);
    int rem = lim - pos;
    return new HeapByteBufferR(hb,
                                    -1,
                                    0,
                                    rem,
                                    rem,
                                    pos + offset);
}

新创建一个 ByteBuffer,将缓存区分片,设置一个子缓冲区,实际上内存还是共享的,数据发生改变,两个缓冲区读取的数据都会是改变后的。

总结

Buffer 最重要的三个属性:position、limit、capacity。牢记这三个属性的含义及读写切换时,设置值是如何变化的,Buffer 的核心知识点就掌握了。

更多文章见:https://nc2era.com


本文作者:aloof_

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

正文完
 0