NIO 二 创建缓冲区

一 前言

  前一篇博客中介绍了缓冲区Buffer,其中大量的篇幅在描述缓冲区的4个核心参数的设置/访问API,这是所有Buffer类设计的基础,这一篇则在此基础上介绍如何创建不同类型的缓冲区。

  创建缓冲区大致分为两种模式,第一种基于JVM数组对象创建的缓冲区,内存分配在JVM中;第二种是直接缓冲区,数据操作直接和核心交互。

  两者的差异主要在于内存分配上不同,另外直接缓冲区在分配和回收内存空间时,消耗的时间要高于非直接缓冲区,但是因省略了中间缓冲区的数据传递过程,整体的性能要高于非直接缓冲区。

二 通过显式的数组对象创建

  创建非直接缓冲区的第一种方法是通过一个数组对象来构建,Buffer的直接派生类都提供了静态的wrap(XX[])方法,这一组静态的wrap方法要求传入一个既定数据类型的数组对象,如CharBuffer:

public abstract class CharBuffer extends Buffer implements Comparable<CharBuffer>, Appendable, CharSequence, Readable {
	...
    public static CharBuffer wrap(char[] array) {
        return wrap(array, 0, array.length);
    }
	
	public static CharBuffer wrap(char[] array, int offset, int length) {
		try {
			// 返回的是HeapCharBuffer,并且把数组对象引用作为构造参数
			return new HeapCharBuffer(array, offset, length);
		} catch (IllegalArgumentException x) {
			throw new IndexOutOfBoundsException();
		}
	}
	...
}

  通过源码可以看出来通过wrap方法创建的缓冲区实际类型是HeapXXBuffer,并且偏移量offset参数为0,跟进HeapCharBuffer的构造函数:

class HeapCharBuffer extends CharBuffer {
	...
    HeapCharBuffer(char[] buf, int off, int len) {
		// mark=-1
		// position=0
		// limit=position + 数组长度
		// capacity=数组长度
		// offset=0
        super(-1, off, off + len, buf.length, buf, 0);
    }
	...
}

  兜兜圈圈,HeapCharBuffer实际上还是用父类CharBuffer进行构造的,回到CharBuffer:

public abstract class CharBuffer extends Buffer implements Comparable<CharBuffer>, Appendable, CharSequence, Readable {
	...
    CharBuffer(int mark, int pos, int lim, int cap, char[] hb, int offset) {
		// mark=-1
		// position=0
		// limit=position + 数组长度
		// capacity=数组长度 
        super(mark, pos, lim, cap);
		// 成员hb是char[]类型,接收构造参数传入的char[]对象引用
        this.hb = hb;
		// 偏移量由CharBuffer保存,通过wrap构造时,偏移量为0
        this.offset = offset;
    }
	...
}

  看到这里就比较清楚了,实际上Buffer作为缓冲区最大佬的基类,只负责4个核心参数的管理,7个直接派生类则内部持有一个对应类型的数组成员,以及一个偏移量成员。

  通过wrap()方法构造一个CharBuffer,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println("capacity:" + charBuffer.capacity() + " limit:" + charBuffer.limit() + " position:" + charBuffer.position());

输出结果:
capacity:3 limit:3 position:0

  显式的传入数组对象的方式来创建缓冲区,首先它是非直接缓冲区,其次可以通过初始化数组对象来填充缓冲区数据,但这样的用法并不会太多,基本上都是为了方便数组操作才会临时将一个数组对象包装成缓冲区来用,实际上也只是图缓冲区API的便利。

三 通过隐式的数组对象创建

  Buffer的直接派生类还提供了allocate方法来创建一个指定长度的缓冲区,依然拿CharBuffer举例:

public abstract class CharBuffer extends Buffer implements Comparable<CharBuffer>, Appendable, CharSequence, Readable {
	...
   public static CharBuffer allocate(int capacity) {
   		// 参数不能为负
        if (capacity < 0)
            throw new IllegalArgumentException();
		// 实际上还是创建了一个HeapCharBuffer,只不过没有数组对象引用作为参数了
        return new HeapCharBuffer(capacity, capacity);
    }
	...
}

  章节二 通过显式的数组对象创建中介绍过,如果显式的传入数组对象来创建缓冲区,那么核心参数值是通过数组的长度来确定,虽然allocate方法仅要求传入一个长度,但实际上和wrap方法并无太大差异:

class HeapCharBuffer extends CharBuffer {
	...
    HeapCharBuffer(int cap, int lim) {
		// 实际上这里还是通过char[]来构造的,只不过是HeapCharBuffer帮我们创建了一个参数长度的char数组
        super(-1, 0, lim, cap, new char[cap], 0);
    }
	...
}

  到这里无需继续跟进源码了,因为和wrap方法的后半段处理逻辑一样,通过allocat方法创建缓冲区示例如下:

CharBuffer charBuffer = CharBuffer.allocate(3);
System.out.println("capacity:" + charBuffer.capacity() + " limit:" + charBuffer.limit() + " position:" + charBuffer.position());

输出结果:
capacity:3 limit:3 position:0

  和wrap方法不同的是,allocate没有办法在构造缓冲区时就将数据初始化,因为数组对象是新鲜出炉的。

四 创建直接缓冲区

  前文中创建的缓冲区都是HeapXXBuffer,非直接缓冲区,并且都是基于数组的,尤其是wrap方式创建的缓冲区,因为底层是用的数组对象引用,一旦数组元素发生变化,缓冲区数据也会变化。

  这里再介绍下直接缓冲区,首先说说非直接缓冲区的数据交互过程:

非直接缓冲区数据处理过程

  直接缓冲区则省略了从内核态到用户态的缓存复制过程,通过物理内存映射的方式直接操作数据。

  而系统层面对面数据的操作都是以连续字节的模式传输的,所以创建直接缓冲区仅ByteBuffer支持:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> { 
	...
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
	...
}

  从源码中可以看到allocateDirect返回的是DirectByteBuffer类型:

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {
	...
	DirectByteBuffer(int cap) {
		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)) {
			address = base + ps - (base & (ps - 1));
		} else {
			address = base;
		}
		cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
		att = null;
    }
	...
}

  DirectByteBuffer是直接通过本地方法来分配内存的,也就是说它的实例并不基于数组对象实现:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(3);
System.out.println(byteBuffer.hasArray());
System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());

输出结果:
false
capacity:3 limit:3 position:0

五 复制缓冲区

  前文中介绍的是都基于数组,或者直接分配内存的方式来创建缓冲区,实际上NIO还支持基于缓冲区对象的复制,大体上分为三类:

  1. 浅拷贝
  2. 只读拷贝
  3. 切分

  需要注意的是,上述三种方法均会创建一个新的Buffer对象,但是共用缓存数据。

5.1 浅拷贝缓冲区

  duplicate方法会创建一个新的缓冲区对象,这意味着和原缓冲区拥有相互独立的核心参数配置,但是需要注意的是复制缓冲区对象和原缓冲区对象是共用缓存数据的:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(3);
ByteBuffer newByteBuffer = byteBuffer.duplicate();
newByteBuffer.limit(2);
System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
System.out.println("capacity:" + newByteBuffer.capacity() + " limit:" + newByteBuffer.limit() + " position:" + newByteBuffer.position());
byteBuffer.put((byte)1);
for (int i = 0; i < byteBuffer.limit(); i++) {
	System.out.print(byteBuffer.get(i));
}
System.out.println();
for (int i = 0; i < newByteBuffer.limit(); i++) {
	System.out.print(byteBuffer.get(i));
}

输出结果:
capacity:3 limit:3 position:0
capacity:3 limit:2 position:0
100
10

5.2 只读拷贝

  在上一篇博客中提到过,缓冲区还有一个是否只读的属性,可通过方法isReadOnly来访问此属性,那么创建只读缓冲区的方式是通过Buffer对象的asReadOnlyBuffer实现的,除只读属性其他特性和duplicate一致:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(3);
System.out.println(byteBuffer.isReadOnly());
ByteBuffer newByteBuffer = byteBuffer.asReadOnlyBuffer();
System.out.println(newByteBuffer.isReadOnly());

输出结果:
false
true

5.3 缓存切分

  缓存切分是指从原缓冲区的下一个可读写位置上,切分一个长度为原缓冲区limit-position大小的新缓冲区,slice方法实现了此功能,使用示例如下:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(3);
byteBuffer.put((byte) 1);
byteBuffer.put((byte) 2);
byteBuffer.put((byte) 3);
byteBuffer.position(2);
System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
for (int i = 0; i < byteBuffer.limit(); i++) {
	System.out.println(byteBuffer.get(i));
}
ByteBuffer newByteBuffer = byteBuffer.slice();
System.out.println("capacity:" + newByteBuffer.capacity() + " limit:" + newByteBuffer.limit() + " position:" + newByteBuffer.position());
for (int i = 0; i < newByteBuffer.limit(); i++) {
	System.out.println(byteBuffer.get(i));
}

输出结果:
capacity:3 limit:3 position:2
1
2
3
capacity:1 limit:1 position:0
1

六 缓存数据格式转换

  缓存的数据格式转换和直接内存分配一样,都是ByteBuffer的专属,因为无论是网络数据传输还是系统层面的磁盘数据操作,实际上都是对Byte数据进行传递,并在终端上进行格式转换,最终展示给用户,ByteBuffe提供了将缓存区转换为其他六种缓冲区类型的转换方法asXXBuffer。

  转换的实现是创建一个对应转换类型的缓冲区对象,并且共用缓存数据,转换时从原缓冲区的position位置开始,返回的新缓冲区尺寸为原缓冲区的(limit-position)/数据格式占位:

ByteBuffer byteBuffer = ByteBuffer.wrap("测试数据格式转换".getBytes());
byteBuffer.position(2);
byteBuffer.limit(10);
System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
CharBuffer charBuffer  = byteBuffer.asCharBuffer();
System.out.println("capacity:" + charBuffer.capacity() + " limit:" + charBuffer.limit() + " position:" + charBuffer.position());
for (int i = 0; i < charBuffer.limit(); i++) {
	System.out.println(charBuffer.get(i));
}

输出结果:
capacity:24 limit:10 position:2
capacity:4 limit:4 position:0
诨
꾕

냦

  大家没看错,CharBuffer中输出的数据确实乱码,这是因为CharBuffer对象的get方法是以UTF-16BE格式进行码制转换的,但是存储数据的时候是系统默认的UTF-8。

  这里用CharBuffer举例也是要强调下,进行缓冲数据格式转换的时候一定要注意编码格式,我们有很多办法解决此问题,只要保证编码和解码的格式一致即可,如编码时指定格式:

ByteBuffer byteBuffer = ByteBuffer.wrap("测试数据格式转换".getBytes("UTF-16BE"));
byteBuffer.position(2);
byteBuffer.limit(10);
System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
CharBuffer charBuffer  = byteBuffer.asCharBuffer();
System.out.println("capacity:" + charBuffer.capacity() + " limit:" + charBuffer.limit() + " position:" + charBuffer.position());
for (int i = 0; i < charBuffer.limit(); i++) {
	System.out.println(charBuffer.get(i));
}

输出结果:
capacity:16 limit:10 position:2
capacity:4 limit:4 position:0
试
数
据
格

  然后我们看到新的CharBuffer长度4,这是因为char占2个byte,所以(10-2)/2得到了最终CharBuffer对象的长度。

  ByteBuffer还提供了其他缓冲区类型的转换方法,用法和asCharBuffer一致,不再赘述。

七 结语

  如果想关注更多硬技能的分享,可以参考积少成多系列传送门,未来每一篇关于硬技能的分享都会在传送门中更新链接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬睡客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值