一文搞懂ByteBuffer使用与原理

Dear 丶 2023-09-25 21:50 219阅读 0赞

前言

已知NIO中有三大组件:ChannelBufferSelector。那么Buffer的作用就是提供一个缓冲区,用于用户程序和Channel之间进行数据读写,也就是用户程序中可以使用BufferChannel写入数据,也可以使用BufferChannel读取数据。

ByteBufferBuffer子类,是字节缓冲区,特点如下所示。

  1. 大小不可变。一旦创建,无法改变其容量大小,无法扩容或者缩容;
  2. 读写灵活。内部通过指针移动来实现灵活读写;
  3. 支持堆上内存分配和直接内存分配。

本文将对ByteBuffer的相关概念,常用API以及使用案例进行分析。全文约1万字,知识点脑图如下。

format_png

正文

一. Buffer

NIO中,八大基础数据类型中除了boolean外,都有相应的Buffer的实现,类图如下所示。

format_png 1

Buffer类对各种基础数据类型的缓冲区做了顶层抽象,所以要了解ByteBuffer,首先应该学习Buffer类。

  1. Buffer的属性

所有缓冲区结构都有如下属性。






















属性 说明
int position 位置索引。代表下一次将要操作的元素的位置,默认初始为0,位置索引最小为0,最大为limit
int limit 限制索引。限制索引及之后的索引位置上的元素都不能操作,限制索引最小为0,最大为capacity
int capacity 容量。缓冲区的最大元素个数,创建缓冲区时指定,最小为0,不能改变

三者之间的大小关系应该是:0 <= position <= limit <= capacity,图示如下。

format_png 2

除此之外,还有一个属性叫做mark,如下所示。














属性 说明
int mark 标记索引。mark会标记一个索引,在Buffer#reset调用时,将position重置为markmark不是必须的,但是当定义mark后,其最小为0,最大为position

关于mark还有如下两点说明。

  1. positionlimit一旦小于markmark会被丢弃;
  2. 没有定义mark时如果调用了Buffer#reset则会抛出InvalidMarkException

  3. Buffer的读模式

Buffer有两种模式,读模式和写模式,在读模式下,可以读取缓冲区中的数据。那么对于一个缓冲区,要读取数据时,分为两步。

  1. 拿到position位置索引;
  2. position位置的数据。

那么Buffer提供了nextGetIndex() 方法和nextGetIndex(int nb) 方法来获取position,先看一下nextGetIndex() 方法的实现。

  1. final int nextGetIndex() {
  2. // limit位置是不可操作的
  3. if (position >= limit) {
  4. throw new BufferUnderflowException();
  5. }
  6. // 返回当前position
  7. // 然后position后移一个位置
  8. return position++;
  9. }
  10. 复制代码

nextGetIndex() 方法首先校验一下position是否大于等于limit,因为limit及之后的位置都是不可操作的,所以只要满足position大于等于limit则抛出异常,然后返回当前的position(也就是当前可操作的位置),最后position后移一位。

nextGetIndex(int nb) 方法,则是用于Buffer的子类ByteBuffer使用,因为ByteBuffer的一个元素就是一个字节,而如果想要通过ByteBuffer获取一个整形数据,那么此时就需要连续读取四个字节。nextGetIndex(int nb) 方法如下所示。

  1. final int nextGetIndex(int nb) {
  2. // 判断一下剩余可操作元素是否够本次获取
  3. if (limit - position < nb) {
  4. throw new BufferUnderflowException();
  5. }
  6. // 暂存当前position
  7. int p = position;
  8. // 然后position后移nb个位置
  9. position += nb;
  10. // 返回暂存的position
  11. return p;
  12. }
  13. 复制代码

拿到position后,实际的读取数据,由Buffer的子类来实现。

  1. Buffer的写模式

有读就有写,在Buffer的写模式下,写入数据也是分为两步。

  1. 拿到position位置索引;
  2. 写入数据到position位置。

写模式下,Buffer同样为获取position提供了两个方法,如下所示。

  1. final int nextPutIndex() {
  2. // limit位置是不可操作的
  3. if (position >= limit) {
  4. throw new BufferOverflowException();
  5. }
  6. // 返回当前position
  7. // 然后position后移一个位置
  8. return position++;
  9. }
  10. final int nextPutIndex(int nb) {
  11. // 判断一下剩余可操作元素是否够本次写入
  12. if (limit - position < nb) {
  13. throw new BufferOverflowException();
  14. }
  15. // 暂存当前position
  16. int p = position;
  17. // 然后position后移nb个位置
  18. position += nb;
  19. // 返回暂存的position
  20. return p;
  21. }
  22. 复制代码

同样,拿到position后,实际的写入数据,由Buffer的子类来实现。

  1. Buffer读写模式切换

Buffer提供了读模式和写模式,同一时间Buffer只能在同一模式下工作,相应的,Buffer提供了对应的方法来做读写模式切换。

首先是读模式切换到写模式,先看如下示意图。

format_png 3

上图中的情况是缓冲区中的数据已经全部被读完,那么此时如果要切换到写模式,对应的方法是clear() 方法,如下所示。

  1. public final Buffer clear() {
  2. // 重置position为0
  3. position = 0;
  4. // 设置limit为capacity
  5. limit = capacity;
  6. // 重置mark为-1
  7. mark = -1;
  8. return this;
  9. }
  10. 复制代码

注意,虽然方法名叫做clear(),但是实际缓冲区中的数据并没有被清除,而只是将位置索引position,限制索引limit进行了重置,同时清除了标记状态(也就是将mark设置为-1)。切换到写模式后,缓冲区示意图如下所示。

format_png 4

然后是写模式切换到读模式,先看如下示意图。

format_png 5

数据已经写入完毕了,此时如果要切换到读模式,对应的方法是flip(),如下所示。

  1. public final Buffer flip() {
  2. // 因为position位置还没写入数据
  3. // 所以将position位置设置为limit
  4. limit = position;
  5. // 重置position为0
  6. position = 0;
  7. // 重置mark为-1
  8. mark = -1;
  9. return this;
  10. }
  11. 复制代码

因为position永远代表下一个可操作的位置,那么在写模式下,position代表下一个写入的位置,那么其实就还没有数据写入,所以调用flip() 方法后,首先将position位置设置为limit,表示数据最多读取到limit的上一个位置,然后重置positionmark。切换到读模式后,缓冲区示意图如下所示。

format_png 6

  1. Buffer的rewind操作

在使用Buffer时,可以针对已经操作的区域进行重操作,假设缓冲区示意图如下。

format_png 7

再看一下rewind() 方法的实现,如下所示。

  1. public final Buffer rewind() {
  2. // 重置position为0
  3. position = 0;
  4. // 清除mark
  5. mark = -1;
  6. return this;
  7. }
  8. 复制代码

主要就是将位置索引position重置为0,这样就能重新操作已经操作过的位置了,同时如果启用了mark,那么还会清除mark,也就是重置mark为-1。rewind() 方法调用后的缓冲区示意图如下所示。

format_png 8

  1. Buffer的reset操作

在使用Buffer时,可以启用mark来标记一个已经操作过的位置,假设缓冲区示意图如下。

format_png 9

再看一下reset() 方法的实现,如下所示。

  1. public final Buffer reset() {
  2. int m = mark;
  3. // 只要启用mark那么mark就不能为负数
  4. if (m < 0) {
  5. throw new InvalidMarkException();
  6. }
  7. // 将position重置为mark
  8. position = m;
  9. return this;
  10. }
  11. 复制代码

在没有启用mark时,mark为-1,只要启用了mark,那么mark就不能为负数。在reset() 中主要就是将位置索引position重新设置到mark标记的位置,以实现对mark标记的位置及之后的位置进行重新操作。reset() 方法调用后的缓冲区示意图如下所示。

format_png 10

二. ByteBuffer

在上一节主要对Buffer进行了一个说明,那么本节会在上一节的基础上,对ByteBuffer及其实现进行学习。

  1. ByteBuffer的属性

ByteBuffer相较于Buffer,多了如下三个属性。






















属性 说明
byte[] hb 字节数组。仅HeapByteBuffer会使用到,HeapByteBuffer的数据存储在hb
int offset 偏移量。仅HeapByteBuffer会使用到,后面会详细说明
isReadOnly 是否只读。仅HeapByteBuffer会使用到,后面会详细说明

NIO中为ByteBuffer分配内存时,可以有两种方式。

  1. 在堆上分配内存,此时得到HeapByteBuffer
  2. 在直接内存中分配内存,此时得到DirectByteBuffer

类图如下所示。

format_png 11

因为DirectByteBuffer是分配在直接内存中,肯定无法像HeapByteBuffer一样将数据存储在字节数组,所以DirectByteBuffer会通过一个address字段来标识数据所在直接内存的开始地址。address字段定义在Buffer中,如下所示。

  1. long address;
  2. 复制代码
  1. ByteBuffer的创建

ByteBuffer提供了如下四个方法用于创建ByteBuffer,如下所示。


























方法 说明
allocate(int capacity) 在堆上分配一个新的字节缓冲区。说明如下:
1. 创建出来后,position为0,并且limit会取值为capacity
2. 创建出来的实际为HeapByteBuffer,其内部使用一个字节数组hb存储元素;
3. 初始时hb中所有元素为0
allocateDirect(int capacity) 在直接内存中分配一个新的字节缓冲区。说明如下:
1. 创建出来后,position为0,并且limit会取值为capacity
2. 创建出来的实际为DirectByteBuffer,是基于操作系统创建的内存区域作为缓冲区;
3. 初始时所有元素为0
wrap(byte[] array) 将字节数组包装到字节缓冲区中。说明如下:
1. 创建出来的是HeapByteBuffer,其内部的hb字节数组就会使用传入的array
2. 改变HeapByteBuffer会影响array,改变array会影响HeapByteBuffer
3. 得到的HeapByteBufferlimitcapacity均取值为array.length
4. position此时都为0
wrap(byte[] array, int off, int length) 将字节数组包装到字节缓冲区,说明如下。
1. 创建出来的是HeapByteBuffer,其内部的hb字节数组就会使用传入的array
2. 改变HeapByteBuffer会影响array,改变array会影响HeapByteBuffer
3. capacity取值为array.length
4. limit取值为off + length
5. position取值为off

下面结合源码,分析一下上述四种创建方式。

首先是allocate(int capacity),如下所示。

  1. public static ByteBuffer allocate(int capacity) {
  2. if (capacity < 0) {
  3. throw new IllegalArgumentException();
  4. }
  5. // 直接创建HeapByteBuffer
  6. // HeapByteBuffer(int cap, int lim)
  7. return new HeapByteBuffer(capacity, capacity);
  8. }
  9. 复制代码

然后是allocateDirect(int capacity),如下所示。

  1. public static ByteBuffer allocateDirect(int capacity) {
  2. return new DirectByteBuffer(capacity);
  3. }
  4. DirectByteBuffer(int cap) {
  5. // MappedByteBuffer(int mark, int pos, int lim, int cap)
  6. super(-1, 0, cap, cap);
  7. boolean pa = VM.isDirectMemoryPageAligned();
  8. int ps = Bits.pageSize();
  9. long size = Math.max(1L, (long)cap + (pa ? ps : 0));
  10. Bits.reserveMemory(size, cap);
  11. long base = 0;
  12. try {
  13. // 分配堆外内存
  14. base = unsafe.allocateMemory(size);
  15. } catch (OutOfMemoryError x) {
  16. Bits.unreserveMemory(size, cap);
  17. throw x;
  18. }
  19. unsafe.setMemory(base, size, (byte) 0);
  20. // 计算堆外内存起始地址
  21. if (pa && (base % ps != 0)) {
  22. address = base + ps - (base & (ps - 1));
  23. } else {
  24. address = base;
  25. }
  26. // 通过虚引用的手段来监视DirectByteBuffer是否被垃圾回收
  27. // 从而可以及时的释放堆外内存空间
  28. cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
  29. att = null;
  30. }
  31. 复制代码

然后是wrap(byte[] array),如下所示。

  1. public static ByteBuffer wrap(byte[] array) {
  2. return wrap(array, 0, array.length);
  3. }
  4. 复制代码

其实wrap(byte[] array) 方法就是调用的wrap(byte[] array, int off, int length),下面直接看wrap(byte[] array, int off, int length) 方法的实现。

  1. public static ByteBuffer wrap(byte[] array, int off, int length) {
  2. try {
  3. return new HeapByteBuffer(array, off, length);
  4. } catch (IllegalArgumentException x) {
  5. throw new IndexOutOfBoundsException();
  6. }
  7. }
  8. 复制代码

这里先简单说明一下上述方法中的offlength这两个参数的含义。

  1. off就是表示字节数组封装到字节缓冲区后,position的位置,所以有position = off
  2. length简单理解就是用于计算limit,即limit = position + length。其实length是理解为字节数组封装到字节缓冲区后,要使用的字节数组的长度。

下面给出一张wrap(byte[] array, int off, int length) 方法的作用示意图。

format_png 12

最后说明一点,无论是wrap(byte[] array) 还是wrap(byte[] array, int off, int length) 方法,均构造的是HeapByteBuffer

  1. ByteBuffer的slice操作

ByteBuffer中定义了一个抽象方法叫做slice(),用于在已有的ByteBuffer上得到一个新的ByteBuffer,两个ByteBufferpositionlimitcapacitymark都是独立的,但是底层存储数据的内存区域是一样的,那么相应的,对其中任何一个ByteBuffer做更改,会影响到另外一个ByteBuffer

下面先看一下HeapByteBufferslice() 方法的实现。

  1. public ByteBuffer slice() {
  2. return new HeapByteBuffer(hb, -1, 0, this.remaining(), this.remaining(), this.position() + offset);
  3. }
  4. public final int remaining() {
  5. return limit - position;
  6. }
  7. protected HeapByteBuffer(byte[] buf, int mark, int pos, int lim, int cap, int off) {
  8. super(mark, pos, lim, cap, buf, off);
  9. }
  10. ByteBuffer(int mark, int pos, int lim, int cap,
  11. byte[] hb, int offset) {
  12. super(mark, pos, lim, cap);
  13. this.hb = hb;
  14. this.offset = offset;
  15. }
  16. 复制代码

新的HeapByteBuffermark重置为了-1,position重置为了0,limit等于capacity等于老的HeapByteBuffer的未操作数据的长度(老的limit - posittion)。

此外,两个HeapByteBuffer存储数据的字节数组hb是同一个,且新的HeapByteBufferoffset等于老的HeapByteBufferposition,什么意思呢,先看下面这张图。

format_png 13

意思就是,在新的HeapByteBuffer中,操作position位置的元素,实际是在操作hb[position + offset] 位置的元素,那么这里也就解释了ByteBufferoffset属性的作用,就是表示要操作字节数组时的索引偏移量。

有了上面对HeapByteBuffer的理解,那么现在再看DirectByteBuffer就显得很简单了,DirectByteBufferslice() 方法的实现如下所示。

  1. public ByteBuffer slice() {
  2. int pos = this.position();
  3. int lim = this.limit();
  4. assert (pos <= lim);
  5. int rem = (pos <= lim ? lim - pos : 0);
  6. int off = (pos << 0);
  7. assert (off >= 0);
  8. return new DirectByteBuffer(this, -1, 0, rem, rem, off);
  9. }
  10. DirectByteBuffer(DirectBuffer db,
  11. int mark, int pos, int lim, int cap,
  12. int off) {
  13. super(mark, pos, lim, cap);
  14. address = db.address() + off;
  15. cleaner = null;
  16. att = db;
  17. }
  18. 复制代码

DirectByteBufferslice() 方法的实现和HeapByteBuffer差不多,只不过在HeapByteBuffer中是对字节数组索引有偏移,而在DirectByteBuffer中是对堆外内存地址有偏移,同时偏移量都是老的ByteBufferposition的值。

最后针对slice() 方法,有一点小说明,在DirectByteBufferatt中有这么一段注释。

If this buffer is a view of another buffer then …

这里提到了view,翻译过来叫做视图,其实调用ByteBufferslice() 方法,可以想象成就是为原字节缓冲区创建了一个视图,这个视图和原字节缓冲区共享同一片内存区域,但是有新的一套markpositionlimitcapacity

  1. ByteBuffer的asReadOnlyBuffer操作

ByteBuffer定义了一个抽象方法叫做asReadOnlyBuffer(),会在当前ByteBuffer基础上创建一个新的ByteBuffer,创建出来的ByteBuffer能看见老ByteBuffer的数据(共享同一块内存),但只能读不能写(只读的),同时两个ByteBufferpositionlimitcapacitymark是独立的。

先看一下HeapByteBufferasReadOnlyBuffer() 方法的实现,如下所示。

  1. public ByteBuffer asReadOnlyBuffer() {
  2. return new HeapByteBufferR(hb,
  3. this.markValue(),
  4. this.position(),
  5. this.limit(),
  6. this.capacity(),
  7. offset);
  8. }
  9. protected HeapByteBufferR(byte[] buf,
  10. int mark, int pos, int lim, int cap,
  11. int off) {
  12. super(buf, mark, pos, lim, cap, off);
  13. this.isReadOnly = true;
  14. }
  15. 复制代码

也就是会new一个HeapByteBufferR出来,并且会指定其isReadOnly字段为true,表示只读。HeapByteBufferR继承于HeapByteBuffer,表示只读HeapByteBufferHeapByteBufferR重写了HeapByteBuffer的所有写相关方法,并且在这些写相关方法中抛出ReadOnlyBufferException异常,下面是部分写方法的示例。

  1. public ByteBuffer put(int i, byte x) {
  2. throw new ReadOnlyBufferException();
  3. }
  4. public ByteBuffer put(byte x) {
  5. throw new ReadOnlyBufferException();
  6. }
  7. 复制代码

再看一下DirectByteBufferasReadOnlyBuffer() 方法的实现,如下所示。

  1. public ByteBuffer asReadOnlyBuffer() {
  2. return new DirectByteBufferR(this,
  3. this.markValue(),
  4. this.position(),
  5. this.limit(),
  6. this.capacity(),
  7. 0);
  8. }
  9. DirectByteBufferR(DirectBuffer db,
  10. int mark, int pos, int lim, int cap,
  11. int off) {
  12. super(db, mark, pos, lim, cap, off);
  13. }
  14. 复制代码

也是会new一个只读的DirectByteBufferRDirectByteBufferR继承于DirectByteBuffer并重写了所有写相关方法,并且在这些写相关方法中抛出ReadOnlyBufferException异常。

  1. ByteBuffer的写操作

ByteBuffer中定义了大量写操作相关的抽象方法,如下图所示。

format_png 14

总体可以进行如下归类。

format_png 15

下面将对上述部分写方法结合源码进行说明。

Ⅰ. put(byte)

首先是最简单的put(byte) 方法,作用是往字节缓冲区的position位置写入一个字节,先看一下HeapByteBuffer对其的实现,如下所示。

  1. public ByteBuffer put(byte x) {
  2. hb[ix(nextPutIndex())] = x;
  3. return this;
  4. }
  5. protected int ix(int i) {
  6. return i + offset;
  7. }
  8. // Buffer#nextPutIndex()
  9. final int nextPutIndex() {
  10. if (position >= limit) {
  11. throw new BufferOverflowException();
  12. }
  13. return position++;
  14. }
  15. 复制代码

再看一下DirectByteBufferput(byte) 方法的实现,如下所示。

  1. public ByteBuffer put(byte x) {
  2. unsafe.putByte(ix(nextPutIndex()), ((x)));
  3. return this;
  4. }
  5. private long ix(int i) {
  6. return address + ((long)i << 0);
  7. }
  8. // Buffer#nextPutIndex()
  9. final int nextPutIndex() {
  10. if (position >= limit) {
  11. throw new BufferOverflowException();
  12. }
  13. return position++;
  14. }
  15. 复制代码

都是会调用到Buffer#nextPutIndex() 方法来拿到当前的position,区别是HeapByteBuffer是将字节写入到堆上的数组,而DirectByteBuffer是写在直接内存中。

Ⅱ. put(int, byte)

put(int, byte) 方法能够在指定位置写入一个字节,注意该方法写入字节不会改变position

HeapByteBuffer对其实现如下所示。

  1. public ByteBuffer put(int i, byte x) {
  2. hb[ix(checkIndex(i))] = x;
  3. return this;
  4. }
  5. protected int ix(int i) {
  6. return i + offset;
  7. }
  8. // Buffer#checkIndex(int)
  9. final int checkIndex(int i) {
  10. if ((i < 0) || (i >= limit)) {
  11. throw new IndexOutOfBoundsException();
  12. }
  13. return i;
  14. }
  15. 复制代码

DirectByteBufferput(int, byte) 方法的实现如下所示。

  1. public ByteBuffer put(int i, byte x) {
  2. unsafe.putByte(ix(checkIndex(i)), ((x)));
  3. return this;
  4. }
  5. private long ix(int i) {
  6. return address + ((long) i << 0);
  7. }
  8. // Buffer#checkIndex(int)
  9. final int checkIndex(int i) {
  10. if ((i < 0) || (i >= limit)) {
  11. throw new IndexOutOfBoundsException();
  12. }
  13. return i;
  14. }
  15. 复制代码

Ⅲ. put(byte[], int, int)

put(byte[], int, int) 方法是批量的将字节数组中指定的字节写到ByteBuffer

put(byte[], int, int) 方法并不是抽象方法,在ByteBuffer中定义了其实现,但同时HeapByteBufferDirectByteBuffer也都对其进行了重写。下面分别看一下其实现。

ByteBuffer#put(byte[], int, int) 实现如下所示。

  1. public ByteBuffer put(byte[] src, int offset, int length) {
  2. checkBounds(offset, length, src.length);
  3. if (length > remaining()) {
  4. throw new BufferOverflowException();
  5. }
  6. int end = offset + length;
  7. // 从src的offset索引开始依次将后续的length个字节写到ByteBuffer中
  8. for (int i = offset; i < end; i++) {
  9. this.put(src[i]);
  10. }
  11. return this;
  12. }
  13. 复制代码

ByteBufferput(byte[], int, int) 方法的实现是循环遍历字节数组中每一个需要写入的字节,然后调用put(byte) 方法完成写入,其中offset表示从字节数组的哪一个字节开始写,length表示从offset开始往后的多少个字节需要写入。

由于ByteBufferput(byte[], int, int) 方法的实现的写入效率不高,所以HeapByteBufferDirectByteBuffer都有自己的实现,先看一下HeapByteBufferput(byte[], int, int) 方法的实现,如下所示。

  1. public ByteBuffer put(byte[] src, int offset, int length) {
  2. checkBounds(offset, length, src.length);
  3. if (length > remaining()) {
  4. throw new BufferOverflowException();
  5. }
  6. // 使用了native的拷贝方法来实现更高效的写入
  7. System.arraycopy(src, offset, hb, ix(position()), length);
  8. position(position() + length);
  9. return this;
  10. }
  11. 复制代码

由于HeapByteBuffer存储字节是存储到字节数组中,所以直接使用nativearraycopy() 方法来完成字节数组的拷贝是更为高效的手段。

再看一下DirectByteBufferput(byte[], int, int) 方法的实现,如下所示。

  1. public ByteBuffer put(byte[] src, int offset, int length) {
  2. // 写入字节数大于6时使用native方法来批量写入才更高效
  3. if (((long) length << 0) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
  4. checkBounds(offset, length, src.length);
  5. int pos = position();
  6. int lim = limit();
  7. assert (pos <= lim);
  8. int rem = (pos <= lim ? lim - pos : 0);
  9. if (length > rem) {
  10. throw new BufferOverflowException();
  11. }
  12. // 这里最终会调用到native方法Unsafe#copyMemory来批量写入
  13. Bits.copyFromArray(src, arrayBaseOffset,
  14. (long) offset << 0,
  15. ix(pos),
  16. (long) length << 0);
  17. // 更新position
  18. position(pos + length);
  19. } else {
  20. // 写入字节数小于等于6则遍历每个字节并依次写入会更高效
  21. super.put(src, offset, length);
  22. }
  23. return this;
  24. }
  25. 复制代码

DirectByteBuffer的实现中,并没有直接调用到native方法来批量操作直接内存,而是先做了判断:如果本次批量写入的字节数大于JNI_COPY_FROM_ARRAY_THRESHOLD(默认是6),才调用native方法Unsafe#copyMemory来完成字节在直接内存中的批量写入,否则就还是一个字节一个字节的写入。DirectByteBuffer的做法主要还是考虑到native方法的调用的一个开销,比如就写入一个字节,那肯定是没有必要调用native方法的。

Ⅳ. put(byte[])

put(byte[]) 方法的作用是将一个字节数组的内容全部写入到ByteBuffer,该方法是一个final方法,所以这里看一下ByteBuffer中该方法的实现,如下所示。

  1. public final ByteBuffer put(byte[] src) {
  2. return put(src, 0, src.length);
  3. }
  4. 复制代码

其实就是调用到put(byte[], int, int) 方法来完成批量写入。

Ⅴ. put(ByteBuffer)

put(ByteBuffer) 方法用于将一个ByteBuffer中所有未操作的字节批量写入当前ByteBufferByteBufferHeapByteBufferDirectByteBuffer都有相应的实现,下面分别看一下。

ByteBuffer#put(ByteBuffer) 思路还是一个字节一个字节的写入,实现如下。

  1. public ByteBuffer put(ByteBuffer src) {
  2. if (src == this) {
  3. throw new IllegalArgumentException();
  4. }
  5. if (isReadOnly()) {
  6. throw new ReadOnlyBufferException();
  7. }
  8. // 计算limit - position
  9. int n = src.remaining();
  10. if (n > remaining()) {
  11. throw new BufferOverflowException();
  12. }
  13. // 一个字节一个字节的写入
  14. for (int i = 0; i < n; i++) {
  15. put(src.get());
  16. }
  17. return this;
  18. }
  19. 复制代码

HeapByteBuffer#put(ByteBuffer) 思路是先判断源ByteBuffer的类型,如果源ByteBufferHeapByteBuffer,则调用native方法System#arraycopy完成批量写入,如果源ByteBuffer是在直接内存中分配的,则再判断一下要写入的字节是否大于6,如果大于6就调用native方法Unsafe#copyMemory完成批量写入,否则就一个字节一个字节的写入。实现如下。

  1. public ByteBuffer put(ByteBuffer src) {
  2. if (src instanceof HeapByteBuffer) {
  3. if (src == this) {
  4. throw new IllegalArgumentException();
  5. }
  6. HeapByteBuffer sb = (HeapByteBuffer) src;
  7. // 计算源ByteBuffer剩余的字节数
  8. int n = sb.remaining();
  9. if (n > remaining()) {
  10. throw new BufferOverflowException();
  11. }
  12. // 调用native方法批量写入
  13. System.arraycopy(sb.hb, sb.ix(sb.position()),
  14. hb, ix(position()), n);
  15. // 更新源ByteBuffer的position
  16. sb.position(sb.position() + n);
  17. // 更新当前ByteBuffer的position
  18. position(position() + n);
  19. } else if (src.isDirect()) {
  20. // 计算源ByteBuffer剩余的字节数
  21. int n = src.remaining();
  22. if (n > remaining()) {
  23. throw new BufferOverflowException();
  24. }
  25. // 批量写入字节到当前ByteBuffer的hb字节数组中
  26. src.get(hb, ix(position()), n);
  27. // 更新当前ByteBuffer的position
  28. position(position() + n);
  29. } else {
  30. super.put(src);
  31. }
  32. return this;
  33. }
  34. // DirectByteBuffer#get(byte[], int, int)
  35. public ByteBuffer get(byte[] dst, int offset, int length) {
  36. if (((long) length << 0) > Bits.JNI_COPY_TO_ARRAY_THRESHOLD) {
  37. checkBounds(offset, length, dst.length);
  38. int pos = position();
  39. int lim = limit();
  40. assert (pos <= lim);
  41. int rem = (pos <= lim ? lim - pos : 0);
  42. if (length > rem) {
  43. throw new BufferUnderflowException();
  44. }
  45. Bits.copyToArray(ix(pos), dst, arrayBaseOffset,
  46. (long) offset << 0,
  47. (long) length << 0);
  48. // 更新源ByteBuffer的position
  49. position(pos + length);
  50. } else {
  51. super.get(dst, offset, length);
  52. }
  53. return this;
  54. }
  55. 复制代码

DirectByteBuffer#put(ByteBuffer) 的思路也是先判断源ByteBuffer的类型,如果源ByteBufferDirectByteBuffer,则直接使用native方法Unsafe#copyMemory完成批量写入,如果源ByteBuffer是在堆上分配的,则按照DirectByteBufferput(byte[], int, int) 方法的逻辑完成批量写入。实现如下所示。

  1. public ByteBuffer put(ByteBuffer src) {
  2. if (src instanceof DirectByteBuffer) {
  3. if (src == this) {
  4. throw new IllegalArgumentException();
  5. }
  6. DirectByteBuffer sb = (DirectByteBuffer) src;
  7. int spos = sb.position();
  8. int slim = sb.limit();
  9. assert (spos <= slim);
  10. // 计算源ByteBuffer剩余的字节数
  11. int srem = (spos <= slim ? slim - spos : 0);
  12. int pos = position();
  13. int lim = limit();
  14. assert (pos <= lim);
  15. int rem = (pos <= lim ? lim - pos : 0);
  16. if (srem > rem) {
  17. throw new BufferOverflowException();
  18. }
  19. // 调用native方法完成批量写入
  20. unsafe.copyMemory(sb.ix(spos), ix(pos), (long) srem << 0);
  21. // 更新源ByteBuffer的position
  22. sb.position(spos + srem);
  23. // 更新当前ByteBuffer的position
  24. position(pos + srem);
  25. } else if (src.hb != null) {
  26. int spos = src.position();
  27. int slim = src.limit();
  28. assert (spos <= slim);
  29. // 计算源ByteBuffer剩余的字节数
  30. int srem = (spos <= slim ? slim - spos : 0);
  31. // 调用DirectByteBuffer#put(byte[], int, int)完成批量写入
  32. put(src.hb, src.offset + spos, srem);
  33. // 更新源ByteBuffer的position
  34. src.position(spos + srem);
  35. } else {
  36. super.put(src);
  37. }
  38. return this;
  39. }
  40. // DirectByteBuffer#put(byte[], int, int)
  41. public ByteBuffer put(byte[] src, int offset, int length) {
  42. if (((long) length << 0) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
  43. checkBounds(offset, length, src.length);
  44. int pos = position();
  45. int lim = limit();
  46. assert (pos <= lim);
  47. int rem = (pos <= lim ? lim - pos : 0);
  48. if (length > rem) {
  49. throw new BufferOverflowException();
  50. }
  51. Bits.copyFromArray(src, arrayBaseOffset,
  52. (long) offset << 0,
  53. ix(pos),
  54. (long) length << 0);
  55. // 更新当前ByteBuffer的position
  56. position(pos + length);
  57. } else {
  58. super.put(src, offset, length);
  59. }
  60. return this;
  61. }
  62. 复制代码

最后有一点需要说明,调用put(ByteBuffer) 方法完成批量字节写入后,源ByteBuffer和当前ByteBufferposition都会被更新。

Ⅵ. 字节序

上述的几种put() 方法都是向ByteBuffer写入字节,但其实也是可以直接将charint等基础数据类型写入ByteBuffer,但在分析这些写入基础数据类型到ByteBufferput() 方法以前,有必要对字节序的相关概念进行演示和说明。

已知在Java中一个int是四个字节,而一个字节是8位,那么就以数字23333为例,示意如下。

format_png 16

那么上述的一个int数据,存储在内存中时,如果高位字节存储在内存的低地址,低位字节存储在内存的高地址,这种就称为大端字节序(Big Endian),示意图如下所示。

format_png 17

反之如果低位字节存储在内存的低地址,高位字节存储在内存的高地址,这种就称为小端字节序(Little Endian),示意图如下所示。

format_png 18

上述其实是主机字节序,表示计算机内存中字节的存储顺序。在Java中,数据的存储默认是按照大端字节序来存储的。

然后还有一种叫做网络字节序,表示网络传输中字节的传输顺序,分类如下。

  1. 大端字节序(Big Endian)。从二进制数据的高位开始传输;
  2. 小端字节序(Little Endian)。从二进制数据的低位开始传输。

在网络传输中,默认按照大端字节序来传输。

Ⅶ. putInt(int)

putInt(int) 方法是ByteBuffer定义的用于直接写入一个int的抽象方法,先看HeapByteBuffer的实现,如下所示。

  1. public ByteBuffer putInt(int x) {
  2. // 通过nextPutIndex(4)方法拿到当前position,并让position加4
  3. // 然后调用Bits#putInt完成写入,其中bigEndian默认是true
  4. Bits.putInt(this, ix(nextPutIndex(4)), x, bigEndian);
  5. return this;
  6. }
  7. // Bits#putInt
  8. static void putInt(ByteBuffer bb, int bi, int x, boolean bigEndian) {
  9. if (bigEndian) {
  10. putIntB(bb, bi, x);
  11. } else {
  12. putIntL(bb, bi, x);
  13. }
  14. }
  15. // Bits#putIntB
  16. static void putIntB(ByteBuffer bb, int bi, int x) {
  17. // 通过Bits#int3方法拿到x的第3字节(最高位字节)
  18. // 然后写入到hb字节数组的索引为bi的位置
  19. bb._put(bi , int3(x));
  20. // 通过Bits#int2方法拿到x的第2字节(次高位字节)
  21. // 然后写入到hb字节数组的索引为bi+1的位置
  22. bb._put(bi + 1, int2(x));
  23. // 通过Bits#int1方法拿到x的第1字节(次低位字节)
  24. // 然后写入到hb字节数组的索引为bi+2的位置
  25. bb._put(bi + 2, int1(x));
  26. // 通过Bits#int0方法拿到x的第0字节(最低位字节)
  27. // 然后写入到hb字节数组的索引为bi+3的位置
  28. bb._put(bi + 3, int0(x));
  29. }
  30. // Bits#int3
  31. private static byte int3(int x) {
  32. return (byte) (x >> 24);
  33. }
  34. // HeapByteBuffer#_put
  35. void _put(int i, byte b) {
  36. hb[i] = b;
  37. }
  38. 复制代码

HeapByteBuffer实现的putInt(int) 方法中,会依次将int的高位到低位写入到hb字节数组的低索引到高索引,而在堆中,内存地址是由低到高的,也就是随着数组索引的增加,内存地址也会逐渐增高,所以上述的就是按照大端字节序的方式来直接写入一个int

再看一下DirectByteBufferputInt(int) 方法的实现,如下所示。

  1. public ByteBuffer putInt(int x) {
  2. // 通过nextPutIndex(4)方法拿到当前position,并让position加4
  3. // 通过ix()方法拿到实际要写入的内存地址
  4. putInt(ix(nextPutIndex((1 << 2))), x);
  5. return this;
  6. }
  7. // DirectByteBuffer#putInt(long, int)
  8. private ByteBuffer putInt(long a, int x) {
  9. if (unaligned) {
  10. int y = (x);
  11. unsafe.putInt(a, (nativeByteOrder ? y : Bits.swap(y)));
  12. } else {
  13. // 调用Bits#putInt完成写入,其中bigEndian默认是true
  14. Bits.putInt(a, x, bigEndian);
  15. }
  16. return this;
  17. }
  18. // Bits#putInt
  19. static void putInt(long a, int x, boolean bigEndian) {
  20. if (bigEndian) {
  21. putIntB(a, x);
  22. } else {
  23. putIntL(a, x);
  24. }
  25. }
  26. // Bits#putIntB
  27. static void putIntB(long a, int x) {
  28. // 通过Bits#int3方法拿到x的第3字节(最高位字节)
  29. // 然后写入到直接内存地址为a的位置
  30. _put(a , int3(x));
  31. // 通过Bits#int2方法拿到x的第2字节(次高位字节)
  32. // 然后写入到直接内存地址为a+1的位置
  33. _put(a + 1, int2(x));
  34. // 通过Bits#int1方法拿到x的第1字节(次低位字节)
  35. // 然后写入到直接内存地址为a+2的位置
  36. _put(a + 2, int1(x));
  37. // 通过Bits#int0方法拿到x的第0字节(最低位字节)
  38. // 然后写入到直接内存地址为a+3的位置
  39. _put(a + 3, int0(x));
  40. }
  41. // Bits#int3
  42. private static byte int3(int x) {
  43. return (byte) (x >> 24);
  44. }
  45. // Bits#_put
  46. private static void _put(long a, byte b) {
  47. unsafe.putByte(a, b);
  48. }
  49. 复制代码

DirectByteBuffer的实现中,会依次将int的高位到低位写入到直接内存的低地址到高地址,整体也是一个大端字节序的写入方式。

Ⅷ. putInt(int, int)

putInt(int, int) 方法可以在指定位置写入int,同时也不会更改positionputInt(int, int) 方法实现原理和putInt(int) 一样,故这里不再赘述。

其它的写入非字节的方法,本质和写入int一致,故也不再赘述。

  1. ByteBuffer的读操作

ByteBuffer中定义了大量读操作相关的抽象方法,如下图所示。

format_png 19

总体可以进行如下归类。

format_png 20

下面将对上述部分读方法结合源码进行说明。

Ⅰ. get()

get() 方法用于读取一个字节,HeapByteBuffer的实现如下所示。

  1. public byte get() {
  2. return hb[ix(nextGetIndex())];
  3. }
  4. 复制代码

上述方法是读取字节数组中position索引位置的字节,然后position加1。再看一下DirectByteBufferget() 方法的实现,如下所示。

  1. public byte get() {
  2. return ((unsafe.getByte(ix(nextGetIndex()))));
  3. }
  4. 复制代码

上述方法是基于native方法拿到address + position位置的字节然后position加1。

Ⅱ. get(int)

get(int) 方法用于读取指定位置的字节,HeapByteBuffer的实现如下所示。

  1. public byte get(int i) {
  2. return hb[ix(checkIndex(i))];
  3. }
  4. 复制代码

上述方法会读取字节数组中指定索引位置的字节,注意position不会改变。再看一下DirectByteBufferget(int) 方法的实现,如下所示。

  1. public byte get(int i) {
  2. return ((unsafe.getByte(ix(checkIndex(i)))));
  3. }
  4. 复制代码

上述方法是基于native方法拿到指定位置的字节,同样,position不会改变。

Ⅲ. get(byte[], int, int)

get(byte[], int, int) 方法用于将当前ByteBufferposition位置开始往后的若干字节写入到目标字节数组的指定位置。ByteBufferHeapByteBufferDirectByteBuffer都有相应的实现,下面分别看一下。

ByteBufferget(byte[], int, int) 方法的实现中是一个字节一个字节的读取并写入,如下所示。

  1. public ByteBuffer get(byte[] dst, int offset, int length) {
  2. checkBounds(offset, length, dst.length);
  3. if (length > remaining()) {
  4. throw new BufferUnderflowException();
  5. }
  6. int end = offset + length;
  7. // 写入目标数组的开始位置是offset
  8. // 共写入length个字节
  9. for (int i = offset; i < end; i++) {
  10. dst[i] = get();
  11. }
  12. return this;
  13. }
  14. 复制代码

HeapByteBufferget(byte[], int, int) 方法的实现中,是调用System#arraycopy本地方法来进行批量拷贝写入,效率比一个字节一个字节的读取并写入更高,且最后会更新当前HeapByteBufferposition

  1. public ByteBuffer get(byte[] dst, int offset, int length) {
  2. checkBounds(offset, length, dst.length);
  3. if (length > remaining()) {
  4. throw new BufferUnderflowException();
  5. }
  6. // 调用native方法来批量写入字节到dst字节数组
  7. System.arraycopy(hb, ix(position()), dst, offset, length);
  8. // 更新当前HeapByteBuffer的position
  9. position(position() + length);
  10. return this;
  11. }
  12. 复制代码

DirectByteBufferget(byte[], int, int) 方法的实现中,会先判断需要读取并写入到目标字节数组中的字节数是否大于6,大于6时会调用native方法来批量写入,否则就一个字节一个字节的读取并写入,最终还会更新当前DirectByteBufferposition

  1. public ByteBuffer get(byte[] dst, int offset, int length) {
  2. if (((long) length << 0) > Bits.JNI_COPY_TO_ARRAY_THRESHOLD) {
  3. // 批量写入的字节数大于6个
  4. checkBounds(offset, length, dst.length);
  5. int pos = position();
  6. int lim = limit();
  7. assert (pos <= lim);
  8. int rem = (pos <= lim ? lim - pos : 0);
  9. if (length > rem) {
  10. throw new BufferUnderflowException();
  11. }
  12. // 最终调用到Unsafe#copyMemory方法完成批量拷贝写入
  13. Bits.copyToArray(ix(pos), dst, arrayBaseOffset,
  14. (long) offset << 0,
  15. (long) length << 0);
  16. // 更新当前DirectByteBuffer的position
  17. position(pos + length);
  18. } else {
  19. // 批量写入的字节数小于等于6个
  20. // 则一个字节一个字节的读取并写入
  21. super.get(dst, offset, length);
  22. }
  23. return this;
  24. }
  25. 复制代码

Ⅳ. get(byte[])

get(byte[]) 方法会从当前ByteBufferposition位置开始,读取目标字节数组长度个字节,然后依次写入到目标字节数组。get(byte[]) 方法由ByteBuffer实现,如下所示。

  1. public ByteBuffer get(byte[] dst) {
  2. return get(dst, 0, dst.length);
  3. }
  4. 复制代码

那么本质还是依赖get(byte[], int, int) 方法,只不过将offset指定为了0(表示从dst字节数组的索引为0的位置开始写入),将length指定为了dst.length(表示要写满dst字节数组)。

Ⅴ. getInt()

getInt() 方法表示从ByteBuffer中读取一个int值,先看一下HeapByteBuffer的实现,如下所示。

  1. public int getInt() {
  2. // 通过nextGetIndex(4)拿到当前position,然后position加4
  3. // 默认bigEndian为true,表示以大端字节序的方式读取int
  4. return Bits.getInt(this, ix(nextGetIndex(4)), bigEndian);
  5. }
  6. // Bits#getInt
  7. static int getInt(ByteBuffer bb, int bi, boolean bigEndian) {
  8. return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ;
  9. }
  10. // Bits#getIntB
  11. static int getIntB(ByteBuffer bb, int bi) {
  12. // 依次拿到低索引到高索引的字节
  13. // 这些字节依次对应int值的高位到低位
  14. // 最终调用makeInt()方法拼接成int值
  15. return makeInt(bb._get(bi),
  16. bb._get(bi + 1),
  17. bb._get(bi + 2),
  18. bb._get(bi + 3));
  19. }
  20. // Bits#makeInt
  21. static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
  22. return (((b3) << 24) |
  23. ((b2 & 0xff) << 16) |
  24. ((b1 & 0xff) << 8) |
  25. ((b0 & 0xff)));
  26. }
  27. // HeapByteBuffer#_get
  28. byte _get(int i) {
  29. return hb[i];
  30. }
  31. 复制代码

上述方法的实现中,首先获取到当前HeapByteBufferposition,然后从position位置开始依次读取四个字节,因为默认情况下是大端字节序(也就是写和读都是按照大端字节序的方式),所以读取到的字节应该依次对应int值的高位到低位,所以最终会在Bits#makeInt方法中将四个字节通过错位或的方式得到int值。

再看一下DirectByteBuffergetInt() 方法的实现,如下所示。

  1. public int getInt() {
  2. // 通过nextGetIndex(4)拿到当前position,然后position加4
  3. // 通过ix()方法拿到操作的起始地址是address + position
  4. return getInt(ix(nextGetIndex((1 << 2))));
  5. }
  6. // DirectByteBuffer#getInt(long)
  7. private int getInt(long a) {
  8. if (unaligned) {
  9. int x = unsafe.getInt(a);
  10. return (nativeByteOrder ? x : Bits.swap(x));
  11. }
  12. // 默认bigEndian为true,表示默认大端字节序
  13. return Bits.getInt(a, bigEndian);
  14. }
  15. // Bits#getInt
  16. static int getInt(long a, boolean bigEndian) {
  17. return bigEndian ? getIntB(a) : getIntL(a) ;
  18. }
  19. // Bits#getIntB
  20. static int getIntB(long a) {
  21. // 从低地址拿到int值的高位
  22. // 从高地址拿到int值的低位
  23. // 然后拼接得到最终的int值
  24. return makeInt(_get(a),
  25. _get(a + 1),
  26. _get(a + 2),
  27. _get(a + 3));
  28. }
  29. // Bits#_get
  30. private static byte _get(long a) {
  31. return unsafe.getByte(a);
  32. }
  33. // Bits#makeInt
  34. static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
  35. return (((b3) << 24) |
  36. ((b2 & 0xff) << 16) |
  37. ((b1 & 0xff) << 8) |
  38. ((b0 & 0xff)));
  39. }
  40. 复制代码

DirectByteBuffergetInt() 方法的整体实现思路和HeapByteBuffer是一致的,在默认大端字节序的情况下,从低地址拿到int值的高位字节,从高地址拿到int值的低位字节,最后通过错位或的方式得到最终的int值。请注意,操作完成后,position都会加4,这是因为一个int占四个字节,也就是相当于读取了4个字节。

Ⅵ. getInt(int)

getInt(int) 方法能够从指定位置读取一个int值,实现思路和getInt() 方法完全一致,故这里不再赘述,但需要注意的是,getInt(int) 方法读取一个int值后,不会改变position

其它的非字节的读取,本质和int值的读取一样,故也不再赘述。

  1. ByteBuffer的使用示例

Log4j2日志框架中,最终在将日志进行输出时,对日志内容的处理就有使用到ByteBuffer,下面一起来简单的看一下。(无需关注Log4j2的实现细节)

在将日志内容进行标准输出时,最终是通过OutputStreamManager完成将日志内容输出,它里面有一个字段就是HeapByteBuffer,用于存储日志内容的字节数据。下面先看一下org.apache.logging.log4j.core.layout.TextEncoderHelper#writeEncodedText方法,这里面会有OutputStreamManagerByteBuffer如何被写入的相关逻辑。

  1. private static void writeEncodedText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
  2. final ByteBuffer byteBuf, final ByteBufferDestination destination, CoderResult result) {
  3. ......
  4. if (byteBuf != destination.getByteBuffer()) {
  5. // 这里的byteBuf存储了处理后的日志内容
  6. // 调用flip()方法来进入读模式
  7. byteBuf.flip();
  8. // 这里的destination就是OutputStreamManager
  9. // 这里会将byteBuf的内容写到OutputStreamManager的ByteBuffer中
  10. destination.writeBytes(byteBuf);
  11. // 切换为写模式,也就是position置0,重置limit等于capacity等
  12. byteBuf.clear();
  13. }
  14. }
  15. // OutputStreamManager#writeBytes
  16. public void writeBytes(final ByteBuffer data) {
  17. if (data.remaining() == 0) {
  18. return;
  19. }
  20. synchronized (this) {
  21. ByteBufferDestinationHelper.writeToUnsynchronized(data, this);
  22. }
  23. }
  24. // ByteBufferDestinationHelper#writeToUnsynchronized
  25. public static void writeToUnsynchronized(final ByteBuffer source, final ByteBufferDestination destination) {
  26. // 拿到OutputStreamManager中的HeapByteBuffer
  27. // 这里称OutputStreamManager中的HeapByteBuffer为目标ByteBuffer
  28. ByteBuffer destBuff = destination.getByteBuffer();
  29. // 如果源ByteBuffer剩余可读字节多于目标ByteBuffer剩余可写字节
  30. // 则循环的写满目标ByteBuffer再读取完目标ByteBuffer
  31. // 最终就是需要将源ByteBuffer的字节全部由目标ByteBuffer消费掉
  32. while (source.remaining() > destBuff.remaining()) {
  33. final int originalLimit = source.limit();
  34. // 先将源ByteBuffer的limit设置为当前position + 目标ByetBuffer剩余可写字节数
  35. source.limit(Math.min(source.limit(), source.position() + destBuff.remaining()));
  36. // 将源ByteBuffer当前position到limit的字节写到目标ByteBuffer中
  37. destBuff.put(source);
  38. // 恢复源ByteBuffer的limit
  39. source.limit(originalLimit);
  40. // 目标ByteBuffer先将已有的字节全部标准输出
  41. // 然后返回一个写模式的目标ByteBuffer
  42. destBuff = destination.drain(destBuff);
  43. }
  44. // 到这里说明源ByteBuffer剩余可读字节小于等于目标ByteBuffer剩余可写字节
  45. // 则将源ByteBuffer剩余可读字节全部写到目标ByteBuffer中
  46. // 后续会在其它地方将这部分内容全部标准输出
  47. destBuff.put(source);
  48. }
  49. // OutputStreamManager#drain
  50. public ByteBuffer drain(final ByteBuffer buf) {
  51. flushBuffer(buf);
  52. return buf;
  53. }
  54. // OutputStreamManager#flushBuffer
  55. protected synchronized void flushBuffer(final ByteBuffer buf) {
  56. // 目标ByteBuffer切换为读模式
  57. ((Buffer) buf).flip();
  58. try {
  59. if (buf.remaining() > 0) {
  60. // 拿到HeapByteBuffer中的字节数组
  61. // 最终调用到PrintStream来标准输出字节数组中的字节内容
  62. writeToDestination(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
  63. }
  64. } finally {
  65. // 目标ByteBuffer切换回写模式
  66. buf.clear();
  67. }
  68. }
  69. 复制代码

相信如果阅读完本文,那么上述Log4j2中对于ByteBuffer的使用,肯定都是能看明白的,虽然Log4j2中有大量的基于ByteBuffer的使用,但是最终的标准输出还是基于Java的传统IO来输出的,那么为什么中间还要用ByteBuffer来多处理一下呢,其实也就是因为ByteBuffer在读写字节时会考虑性能问题,会使用到性能更高的native方法来批量的操作字节数据,因此以快著称的Log4j2选择了NIO中的ByteBuffer

  1. ByteBuffer的缺点

如果要讨论ByteBuffer的缺点,其实可以结合第7小节的使用示例来一并讨论。

首先就是读写模式的切换。在第7小节示例中,会发现存在多处调用flip() 方法来切换到读模式,调用clear() 方法来切换到写模式,这种模式的切换,既麻烦,还容易出错。

然后就是无法扩容。在第7小节示例中,有一个细节就是因为ByteBuffer容量太小了,无法一次写完所有字节数据,所以就只能循环的写满读取然后再写满这样子来操作,如果能扩容就不用这么麻烦了。

最后就是线程不安全ByteBuffer自身并没有提供对线程安全的保护,要实现线程安全,需要使用者自己通过其它的并发语义来实现。

总结

本文对ByteBuffer的分析可以参照下图。

format_png 21

为啥NIO中偏分析ByteBuffer呢,因为Netty中的缓存是ByteBuf,其对ByteBuffer做了改良,在下一篇文章中,将对Netty中的缓存ByteBuf进行详细分析。

发表评论

表情:
评论列表 (有 0 条评论,219人围观)

还没有评论,来说两句吧...

相关阅读

    相关 ThreadLocal 原理

    当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。 数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封