Java NIO 主要由下面3部分组成:
在传统IO中,流是基于字节的方式进行读写的。
在NIO中,使用通道(Channel)基于缓冲区数据块的读写。
流是基于字节一个一个的读取和写入。
通道是基于块的方式进行读取和写入。
Buffer 的类结构图如下:
Buffer类结构图
从图中发现java中8中基本的类型,除了boolean外,其它的都有特定的Buffer子类。
每个缓冲区都有这4个属性,无论缓冲区是何种类型都有相同的方法来设置这些值
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
初始值-1,表示未标记。
标记一个位置,方便以后reset重新从该位置读取数据。
public final Buffer mark() {
mark = position;
return this;
}
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
缓冲区中读取或写入的下一个位置。这个位置从0开始,最大值等于缓冲区的大小
//获取缓冲区的位置
public final int position() {
return position;
}
//设置缓冲区的位置
public final Buffer position(int newPosition) {
if ((newPosition > limit) || (newPosition < 0))
throw new IllegalArgumentException();
position = newPosition;
if (mark > position) mark = -1;
return this;
}
//获取limit位置
public final int limit() {
return limit;
}
//设置limit位置
public final Buffer limit(int newLimit) {
if ((newLimit > capacity) || (newLimit < 0))
throw new IllegalArgumentException();
limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
}
缓冲区可以保存元素的最大数量。该值在创建缓存区时指定,一旦创建完成后就不能修改该值。
//获取缓冲区的容量
public final int capacity() {
return capacity;
}
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
从源码中发现,rewind修改了position和mark,而没有修改limit。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
从clear方法中,我们发现Buffer中的数据没有清空,如果通过Buffer.get(i)的方式还是可以访问到数据的。如果再次向缓冲区中写入数据,他会覆盖之前存在的数据。
查看当前位置和limit之间的元素数。
public final int remaining() {
return limit - position;
}
判断当前位置和limit之间是否还有元素
public final boolean hasRemaining() {
return position < limit;
}
ByteBuffer类结果图
从图中我们可以发现 ByteBuffer继承于Buffer类,ByteBuffer是个抽象类,它有两个实现的子类HeapByteBuffer和MappedByteBuffer类
HeapByteBuffer:在堆中创建的缓冲区。就是在jvm中创建的缓冲区。
MappedByteBuffer:直接缓冲区。物理内存中创建缓冲区,而不在堆中创建。
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
我们发现allocate方法创建的缓冲区是创建的HeapByteBuffer实例。
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
}
从堆缓冲区中看出,所谓堆缓冲区就是在堆内存中创建一个byte[]数组。
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
我们发现allocate方法创建的缓冲区是创建的DirectByteBuffer实例。
DirectByteBuffer 构造方法
直接缓冲区是通过java中Unsafe类进行在物理内存中创建缓冲区。
public static ByteBuffer wrap(byte[] array)
public static ByteBuffer wrap(byte[] array, int offset, int length);
可以通过wrap类把字节数组包装成缓冲区ByteBuffer实例。
这里需要注意的的,把array的引用赋值给ByteBuffer对象中字节数组。如果array数组中的值更改,则ByteBuffer中的数据也会更改的。
Paste_Image.png
ByteBuffer可以转换成其它类型的Buffer。例如CharBuffer、IntBuffer 等。
public ByteBuffer compact() {
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining());
limit(capacity());
discardMark();
return this;
}
1、把缓冲区positoin到limit中的元素向前移动positoin位
2、设置position为remaining()
3、 limit为缓冲区容量
4、取消标记
例如:ByteBuffer.allowcate(10);
内容:0 ,1 ,2 ,3 4, 5, 6, 7, 8, 9
0 ,1 ,2 , 3, 4, 5, 6, 7, 8, 9 pos=4 lim=10 cap=10
4, 5, 6, 7, 8, 9, 6, 7, 8, 9 pos=6 lim=10 cap=10
public ByteBuffer slice() {
return new HeapByteBuffer(hb,
-1,
0,
this.remaining(),
this.remaining(),
this.position() + offset);
}
创建一个分片缓冲区。分配缓冲区与主缓冲区共享数据。
分配的起始位置是主缓冲区的position位置
容量为limit-position。
分片缓冲区无法看到主缓冲区positoin之前的元素。
下面我们从缓冲区创建的性能和读取性能两个方面进行性能对比。
public static void directReadWrite() throws Exception {
int time = 10000000;
long start = System.currentTimeMillis();
ByteBuffer buffer = ByteBuffer.allocate(4*time);
for(int i=0;i<time;i++){
buffer.putInt(i);
}
buffer.flip();
for(int i=0;i<time;i++){
buffer.getInt();
}
System.out.println("堆缓冲区读写耗时 :"+(System.currentTimeMillis()-start));
start = System.currentTimeMillis();
ByteBuffer buffer2 = ByteBuffer.allocateDirect(4*time);
for(int i=0;i<time;i++){
buffer2.putInt(i);
}
buffer2.flip();
for(int i=0;i<time;i++){
buffer2.getInt();
}
System.out.println("直接缓冲区读写耗时:"+(System.currentTimeMillis()-start));
}
输出结果:
堆缓冲区创建耗时 :70
直接缓冲区创建耗时:47
从结果中我们发现堆缓冲区读写比直接缓冲区读写耗时更长。
public static void directAllocate() throws Exception {
int time = 10000000;
long start = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
ByteBuffer buffer = ByteBuffer.allocate(4);
}
System.out.println("堆缓冲区创建时间:"+(System.currentTimeMillis()-start));
start = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
ByteBuffer buffer = ByteBuffer.allocateDirect(4);
}
System.out.println("直接缓冲区创建时间:"+(System.currentTimeMillis()-start));
}
输出结果:
堆缓冲区创建时间:73
直接缓冲区创建时间:5146
从结果中发现直接缓冲区创建分配空间比较耗时。
直接缓冲区比较适合读写操作,最好能重复使用直接缓冲区并多次读写的操作。
堆缓冲区比较适合创建新的缓冲区,并且重复读写不会太多的应用。
建议:如果经过性能测试,发现直接缓冲区确实比堆缓冲区效率高才使用直接缓冲区,否则不建议使用直接缓冲区。