Java基础-NIO(1)Buffer

Java NIO是指JDK 1.4版本后java.nio.*包中引入新的I/O库,它跟Java的传统IO有很大区别。

NIO主要有三大核心部分:Channel(通道)Buffer(缓冲区), Selector(选择器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从Channel读取到Buffer中,或者从Buffer写入到Channel中。Selector用于监听多个Channel的事件(比如:连接打开,数据到达)。

Buffer是什么

Buffer(缓冲区)是NIO中另外以后一个重要的概念。Buffer本质上是一块可以写入和读取数据的内存空间,这块内存空间被包装成Buffer对象,并提供了一组方法用来访问该空间。从应用程序的角度来讲,Buffer其实就是一个存储数据的容器。

Buffer属性

Buffer有三个重要的属性:

  • 容量 (capacity) : 表示 Buffer 最大存储数据容量,该值不能为负,一旦声明后不能更改。
  • 界限 (limit): 第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。该值不能为负,且不能大于其capacity。
    在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
    当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当Buffer从写模式切换到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。
  • 位置 (position): 下一个要读取或写入的数据的索引。该值不能为负,且不能大于其limit。
    当写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
    当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

capacity、limit和position示意图如下。

Buffer类型

前面说到Buffer是一个存储数据的容器,容器中存储的数据类型是有限制的,只能存放除boolean类型外的基本类型,比如short、int、char、byte等,每一种基本类型会对应一种Buffer的实现,所以Buffer的常见实现类就有如下几种:

  • ByteBuffer(最常用)
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • MappedByteBuffer

其中MappedByteBuffer有些特别,后面再做专门说明。

Buffer使用

每种数据类型的Buffer都会有对应的读(get)和写(put)两个基本操作,并且有获取Buffer的 capacity 、limit 和 position 3个值的对应方法:int capacity()int limit()int position,并且可以通过Buffer limit(int n)设置limit属性,通过Buffer position(int n)设置position属性,这两个方法返回修改后的Buffer对象。

int remaining()方法可以获取position和limit之间的元素个数,boolean hasRemaining()用来判断缓冲区中是否还有元素。

Buffer clear()方法用来清空缓存区,并返回修改后的Buffer对象。

Buffer flip()方法是将当前的position赋值给limit,然后将position设置为0。flip()可以将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态。

1
2
3
4
5
6
public final Buffer flip() { 
limit = position;
position = 0;
mark = -1;
return this;
}

Rewind()函数与flip()相似,但不会修改limit。它只是将position值设回0。我们可以使用rewind()后退,重读已经被翻转的缓冲区中的数据。

1
2
3
4
5
public final Buffer rewind() { 
position = 0;
mark = -1;
return this;
}

mark和reset
Buffer的mark()方法可以对当前的position做一个标记,当我们对Buffer做完一系列操作之后,再调用reset()方法可以将position重置到这个标记的位置。

创建buffer
通过XxxBuffer.allocate(int capacity)可以创建一个指定容量的Buffer对象。

如下是测试代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class BufferTest {

@Test
public void test1() {

String str = "abcde";

//1.分配一个指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("---------allocate---------");
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());

//2.使用put()方法存入数据到缓冲区
byteBuffer.put(str.getBytes());
System.out.println("---------put---------");
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());

//3.flip()方法切换读取数据模式
byteBuffer.flip();
System.out.println("---------flip---------");
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());

//4.利用get()方法读取缓冲区中的数据
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println("---------get---------");
System.out.println(new String(bytes));
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());

//5.利用rewind()方法可以再次重新读取
byteBuffer.rewind();
System.out.println("---------rewind---------");
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());

//6.clear()方法,清空缓冲区,但是缓冲区中的数据依然存在,处于一个“被遗忘”的状态
byteBuffer.clear();
System.out.println("---------clear---------");
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());
System.out.println((char) byteBuffer.get());
}

@Test
public void test2() {
String str = "abcde";

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(str.getBytes());
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];

/*
get(byte[] dst, int offset, int length)方法:
从缓冲区读取length个元素,写入到byte数组索引为offset的位置。
如果缓冲区中的元素数量不够(读取时会超过limit值),或者byte数组的容量不够,都会报错。
*/

byteBuffer.get(bytes, 0, 2);
System.out.println(new String(bytes));
System.out.println(byteBuffer.position());

//mark()方法:标记
byteBuffer.mark();
byteBuffer.get(bytes, 1, 3);
System.out.println(new String(bytes));
System.out.println(byteBuffer.position());

//reset()方法:恢复到mark的位置
byteBuffer.reset();
System.out.println(byteBuffer.position());
}

@Test
public void test3() {
//创建直接缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);

//isDrect()方法可以判断是否是直接缓冲区
System.out.println(byteBuffer.isDirect());

}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#test1方法输出结果:

---------allocate---------
1024
1024
0
---------put---------
1024
1024
5
---------flip---------
1024
5
0
---------get---------
abcde
1024
5
5
---------rewind---------
1024
5
0
---------clear---------
1024
1024
0
a

#test2方法打印输出:

ab
2
acde
5
2

#test3方法打印输出:

true

直接缓冲区

上面的示例代码中获取ByteBuffer实例时,除了使用allocate(int n)方法,还可以通过allocateDirect(int n)方法获得,这两者得到的Buffer对象有什么区别呢?
前面提到Buffer对象本质上是一块内存空间,从逻辑上讲,allocate(int n)方法创建的Buffer分配的是JVM的堆内存,而allocateDirect(int n)方法创建的Buffer则是直接分配的物理内存,是操作系统级别的。两种方式的读写效率如下图。

可以看出,如果是数据量比较小的情况下它俩的操作效率基本是相同的,没有多大区别。当数据量级非常大的时候,allocateDirect(int n)的优势就体现出来了,它的读写操作效率会更快许多。

allocateDirect的适用场景
因为allocateDirect(int n)是与操作系统内存直接挂钩的,它能够与操作系统更兼容更能够提高io操作速度,但是它消耗的资源比较大,什么场景下我们需要选择allocateDirect(int n)方式呢。

Java官方推荐当使用字节的缓冲区时,如果在操作大型文件以及生命周期很长的时候,才建议使用allocateDirect(int n)方法,因为如果生命周期太短,我们不可能频繁的创建这种缓冲区,频繁创建对内存开销实在是太大很明显不适合,因此,需要生命周期比较长久并且能够重复使用才能够发挥它的作用。再一个是操作大型文件时建议使用是因为这些量级太小的情况下,直接用allocate(int n)就可以了,根本显示不出allocateDirect(int n)的任何优势,以上就是其比较合理的应用场景,其它的场景建议还是使用allocate(int n)吧。

------ 本文完 ------