使用NIO提升性能

Buffer简介

在JDK1.4之前,我们进行文件/流的读写都是通过java.io包的相关类来进行操作,虽然操作简便,但是性能较差。在JDK1.4引入了java.nio包,提供了相关的通道(Channel)和缓冲(Buffer)来操作,极大的提升了读写性能。

通道是双向的,既可用于读也可用于写数据。它从缓冲读取或写入数据到缓冲区。

基本每个java的基本类型都有一个对应的Buffer,比如byte的ByteBuffer;int的IntBuffer等等,但ByteBuffer是最常用的,是大多数标准I/O操作的接口。

Buffer有3个重要的参数:位置(position)、容量(capacity)、上限(limit)。

  • 位置(position)
    a.在写模式下,标识当前缓冲区的位置,将从position的下个位置开始写数据;
    b.在读模式下,标识当前缓冲区读取的位置,将从此位置之后读取数据。
  • 容量(capacity)
    缓冲区的容量上限
  • 上限(limit)
    a.在写模式下,标识缓冲区的实际上限,它小于或等于容量(capacity)。通常情况下和容量(capacity)相等。
    b.在读模式下,代表可以读取的总容量,和上次写入的数据量相等。

Buffer的创建

Buffer的创建有2种方式:
1.从堆中创建

1
ByteBuffer buffer = ByteBuffer.allocate(1024);

2.从已有的数组创建

1
2
byte[] data = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(data);

重置和清空缓冲区

  • rewind()
    position置为0,mark清除,limit无修改。
    作用:为读取Buffer中有效数据做准备。

  • clear()
    position置为0,mark清除,limit设置为capacity。
    作用:为重新写入Buffer做准备。

  • flip()
    position置为0,mark清除,limit设置为position。
    作用:读写切换时使用

读写缓冲区

常用的操作有:

  • public byte get()
    返回当前position上的数据,并将position向下移动一位。

  • public ByteBuffer get(byte[] dst)
    从缓冲区读取数据到dst数组,并恰当的移动position到合适的位置。

  • public byte get(int index)
    读取指定Index上的数据,不会改变position的位置。

  • public ByteBuffer put(byte b)
    在当前位置写入给定的数据,并将position向后移动一位。

  • public ByteBuffer put(int index,byte b)
    将数据写入缓冲区的index位置。position没有改变。

  • public ByteBuffer put(byte[] src)
    将给定的数组写入当前Buffer。并恰当的移动position到合适的位置。

ByteBuffer提供了非常多的方法,这里只是列举了几个常用的方法。

标志缓冲区

标志(mark)有点像书签,在需要标记的地方mark一下,后续任何时候都可以回到mark的地方。

Buffer提供了了下面2个方法:

  • mark()
    用于记录当前的位置。

    1
    2
    3
    4
    public final Buffer mark() {
    mark = position;
    return this;
    }
  • reset()
    用于恢复到mark的位置,即将position设置为mark。

    1
    2
    3
    4
    5
    6
    7
    public final Buffer reset() {
    int m = mark;
    if (m < 0)
    throw new InvalidMarkException();
    position = m;
    return this;
    }

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ByteBuffer buffer = ByteBuffer.allocate(1024);
for (int i=0;i<10;i++) {
buffer.put((byte)i);
}
buffer.flip(); // 重置position,为读取数据做准备
for (int i=0;i<buffer.limit();i++) {
System.out.print(buffer.get());
if (i == 4) { // 在第5个字节的位置做标记
buffer.mark();
System.out.print("(mark at " + i + ")");
}
}
buffer.reset(); // 回到标记的地方
System.out.println();
System.out.println("reset to mark.");
for (int i=buffer.position();i<buffer.limit();i++) {
System.out.print(buffer.get());
}

输出结果:

1
2
3
01234(mark at 4)56789
reset to mark.
56789

上面的代码中,我们先向下缓冲区写入了10个数字(0到9)。然后循环从缓冲区读取并输出,并在第5个位置做了标记。最后重新回到标记的地方,获取缓冲区剩余的数据输出。

复制缓冲区

复制缓冲区是以原缓冲区为基础,生成一个新的缓冲区。新生成的缓冲区与原缓冲区共享内存,对任意一方的数据改动都是相互可见的,但是2者又各自维护自己的position、limit、mark。这大大增加了程序的灵活性,为同时处理数据提供了可能。

复制缓冲区的方法如下:

1
public ByteBuffer duplicate();

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ByteBuffer buffer = ByteBuffer.allocate(1024);
for (int i=0;i<10;i++) {
buffer.put((byte)i);
}
ByteBuffer buf = buffer.duplicate();
System.out.println("After buffer.duplicate");
System.out.println(buffer);
System.out.println(buf);
buf.flip();
System.out.println("After buf.flip()");
System.out.println(buffer);
System.out.println(buf);
buf.put((byte)100);
System.out.println("After buf.put((byte)100)");
System.out.println(buffer);
System.out.println(buf);
System.out.println("buffer.get(0)=" + buffer.get(0));
System.out.println("buf.get(0)=" + buf.get(0));

输出如下:

1
2
3
4
5
6
7
8
9
10
11
After buffer.duplicate
java.nio.HeapByteBuffer[pos=10 lim=1024 cap=1024]
java.nio.HeapByteBuffer[pos=10 lim=1024 cap=1024]
After buf.flip()
java.nio.HeapByteBuffer[pos=10 lim=1024 cap=1024]
java.nio.HeapByteBuffer[pos=0 lim=10 cap=1024]
After buf.put((byte)100)
java.nio.HeapByteBuffer[pos=10 lim=1024 cap=1024]
java.nio.HeapByteBuffer[pos=1 lim=10 cap=1024]
buffer.get(0)=100
buf.get(0)=100

最开始创建了一个有1024个字节的缓冲区buffer,然后写入了10个数。随后通过duplicate()方法复制了一个缓冲区buf。
可以看到复制后的缓冲区的pos,limit,cap都与原缓冲区一致。
然后使用buf.flip()重置新的缓冲区buf
然后写入了一个数字100,这时2个缓冲区的position和limit都不一样。说明原缓冲区和复制的缓冲区维护了各自的position和limit。
最后打印原来的缓冲区和新的缓冲区的第1个位置的数据,都是100.说明原缓冲区和复制的缓冲区共享内存。

缓冲区分片

利用缓冲区分片,可以将一个大的缓冲区分割成若干个子缓冲区,子缓冲区和父缓冲区共享数据。当需要处理一个缓冲区的一个分片时,使用slice()方法得到一个子缓冲区,然后可以像处理普通缓冲区的方法一样处理子缓冲区。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i=0;i<10;i++) {
buffer.put((byte)i);
}
buffer.position(2);
buffer.limit(6);
// 得到一个从第3个位置开始到第6个位置结束的子缓冲区
ByteBuffer newBuffer = buffer.slice();
// 输出子缓冲区的内容
while (newBuffer.hasRemaining()) {
System.out.print(newBuffer.get());
}
System.out.println();
// 修改子缓冲区
newBuffer.flip();
newBuffer.put((byte)100);
System.out.println(newBuffer.get(0));
// 这里为什么去第3个位置?因为子缓冲区的第1个位置=父缓冲区的第3个位置
System.out.println(buffer.get(2));

输出结果:

1
2
3
2345
100
100

从结果可以看到,newBuffer是buffer的一部分(子缓冲区),而且数据是共享的(最后获取的第3个位置的数字都是100)。

只读缓冲区

可以通过asReadOnlyBuffer()方法得到一个只读缓冲区,该缓冲区与原缓冲区共享内存数据。将缓冲区作为参数传递给某个方法时,无法确认该方法会不会破坏缓冲区的数据,此时可以使用只读缓冲区保证缓冲区数据不会被修改。
同时,因为只读缓冲区和原缓冲区共享内存,因此原缓冲区的数据修改,对只读缓冲区也是可见的。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i=0;i<10;i++) {
buffer.put((byte)i);
}
// 得到一个只读缓冲区
ByteBuffer bb = buffer.asReadOnlyBuffer();
// 重置position,准备读取数据
bb.flip();
while (bb.hasRemaining()) {
System.out.print(bb.get()+",");
}
System.out.println();
// 重置原缓冲区的position,准备开始写入数据
buffer.flip();
// 在原缓冲区写入一个数字100
buffer.put((byte)100);
// 由于上面读取只读缓冲区的数据时,position移动了,所以这里重置position,准备从头开始读取数据
bb.flip();
while (bb.hasRemaining()) {
System.out.print(bb.get()+",");
}

输出结果如下:

1
2
0,1,2,3,4,5,6,7,8,9,
100,1,2,3,4,5,6,7,8,9,

在原缓冲区写入了一个数字100,然后在只读缓冲区也是可以拿到的,说明原缓冲区与只读缓冲区是共享内存数据的。

文件映射到内存

NIO提供了一个将文件映射到内存的方法进行I/O操作,它比常规的基于流的I/O操作快很多。这个操作由FileChannel.map()方法实现。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
RandomAccessFile raf = new RandomAccessFile("d:/urls.txt","rw");
// 得到FileChannel
FileChannel channel = raf.getChannel();
// 得到MappedByteBuffer,映射文件的第1到第1024个位置的内容到Buffer
MappedByteBuffer mbb = channel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
// 循环输出缓冲的数据
while(mbb.hasRemaining()) {
System.out.print((char)mbb.get()); // 这里文本的内容长度小于1024,mbb.get()到文件末尾后是空
}
// 重置position,在文件开始位置写入hello,world
mbb.flip();
mbb.put("hello,world".getBytes());
raf.close();

MappedByteBuffer是ByteBuffer的子类,所以可以像操作ByteBuffer操作它。

Donny wechat
欢迎关注我的个人公众号
打赏,是超越赞的一种表达。
Show comments from Gitment