Java NIO 缓冲区概述

因为一直在研究 Apache Mina,看到底层代码后,发现我对 Java NIO 了解甚少,于是开始深入学习 Java NIO。本文是 Java NIO 系列的第一篇,主要介绍 NIO 缓冲区(Buffer)的基本概念和使用方法。

缓冲区的核心属性

Java NIO 缓冲区有四个核心属性,理解这些属性是掌握缓冲区操作的关键:

  1. capacity:缓冲区可容纳的最大数据量,一旦设置不可改变
  2. position:当前操作位置,读写操作会改变此值
  3. limit:缓冲区第一个不能被读写的元素位置
  4. mark:标记位置,用于后续重置 position

这四个属性的关系是:0 <= mark <= position <= limit <= capacity

缓冲区类型

Java NIO 提供了多种类型的缓冲区,对应不同的数据类型:

缓冲区类型 对应数据类型
ByteBuffer byte
CharBuffer char
ShortBuffer short
IntBuffer int
LongBuffer long
FloatBuffer float
DoubleBuffer double

缓冲区的基本操作

1. 创建缓冲区

1
2
3
4
5
6
7
8
9
// 分配指定容量的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 从现有数组创建缓冲区
byte[] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);

// 创建直接缓冲区(直接在内存中分配,不经过JVM堆)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

2. 写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
// 写入单个字节
buffer.put((byte)'H');
buffer.put((byte)'e');
buffer.put((byte)'l');
buffer.put((byte)'l');
buffer.put((byte)'o');

// 写入指定位置
buffer.put(0, (byte)'M'); // 替换第一个字节

// 写入数组
byte[] data = "World".getBytes();
buffer.put(data);

3. 翻转缓冲区

当写入完成后,需要将缓冲区从写模式切换到读模式:

1
2
// 翻转缓冲区:limit = position, position = 0
buffer.flip();

4. 读取数据

1
2
3
4
5
6
7
8
9
10
11
12
// 读取单个字节
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.print((char) b);
}

// 读取到数组
byte[] dest = new byte[buffer.remaining()];
buffer.get(dest);

// 读取指定位置
buffer.get(0); // 读取第一个字节,不改变position

5. 重置缓冲区

1
2
3
4
5
6
7
8
9
10
11
// 清空缓冲区:position = 0, limit = capacity
buffer.clear();

// 倒带缓冲区:position = 0, limit 不变
buffer.rewind();

// 标记位置
buffer.mark();

// 重置到标记位置
buffer.reset();

缓冲区的工作原理

初始状态

创建缓冲区后,初始状态为:

  • position = 0
  • limit = capacity
  • mark = -1(未定义)

写入数据

写入数据时,position 会逐渐增加,直到达到 limit。

翻转操作

调用 flip() 后:

  • limit = position(设置可读数据的边界)
  • position = 0(从开始位置读取)
  • mark = -1(清除标记)

读取数据

读取数据时,position 会逐渐增加,直到达到 limit。

清空操作

调用 clear() 后:

  • position = 0
  • limit = capacity
  • mark = -1

注意:clear() 只是重置了指针,并没有清除数据。

直接缓冲区与非直接缓冲区

非直接缓冲区

  • 在 JVM 堆中分配内存
  • 读写操作需要在 JVM 堆和 native 内存之间复制数据
  • 创建和销毁速度快
  • 适合小数据量操作

直接缓冲区

  • 在 native 内存中分配内存
  • 读写操作直接操作 native 内存,无需复制
  • 创建和销毁速度慢
  • 适合大数据量操作,特别是需要与通道直接交互的场景
1
2
3
4
5
// 创建直接缓冲区
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

// 检查是否为直接缓冲区
boolean isDirect = buffer.isDirect();

缓冲区的最佳实践

  1. 选择合适的缓冲区类型:根据数据类型选择对应的缓冲区
  2. 合理设置容量:根据实际需要设置缓冲区容量,避免频繁扩容
  3. 使用直接缓冲区:对于大数据量操作,使用直接缓冲区提高性能
  4. 正确管理缓冲区状态:熟练使用 flip(), clear(), rewind() 等方法
  5. 注意缓冲区边界:使用 hasRemaining() 检查是否还有数据可读
  6. 避免频繁创建缓冲区:可以复用缓冲区减少GC压力

代码示例

基本读写操作

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
import java.nio.ByteBuffer;

public class BufferExample {
public static void main(String[] args) {
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 写入数据
String message = "Hello, Java NIO!";
buffer.put(message.getBytes());

// 翻转缓冲区
buffer.flip();

// 读取数据
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println(new String(data));

// 清空缓冲区
buffer.clear();

// 重新写入数据
String newMessage = "Welcome to NIO Buffer!";
buffer.put(newMessage.getBytes());

// 翻转并读取
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
}
}

缓冲区复用

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
import java.nio.CharBuffer;

public class BufferFillData {
private static int index = 0;
private static String[] strings = {
"A random string value",
"The product of an infinite number of monkeys",
"Hey hey we're the Monkees",
"Opening act for the Monkees: jiangfuqiang",
"Help me! Help me!"
};

public static void main(String[] args) {
CharBuffer buffer = CharBuffer.allocate(100);
while (fillBuffer(buffer)) {
buffer.flip();
drainBuffer(buffer);
buffer.clear();
}
}

private static void drainBuffer(CharBuffer buffer) {
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
System.out.println();
}

private static boolean fillBuffer(CharBuffer buffer) {
if (index >= strings.length) {
return false;
}
String string = strings[index++];
for (int i = 0; i < string.length(); i++) {
buffer.put(string.charAt(i));
}
return true;
}
}

缓冲区的高级操作

1. 压缩操作

当缓冲区中还有未读取的数据时,可以使用 compact() 方法将未读取的数据移到缓冲区开头:

1
2
3
4
5
6
7
8
// 读取部分数据
buffer.get(data, 0, 5);

// 压缩缓冲区:将未读取的数据移到开头
buffer.compact();

// 继续写入数据
buffer.put(moreData);

2. 批量操作

1
2
3
4
5
6
7
8
9
10
11
12
// 批量写入
byte[] data = new byte[100];
buffer.put(data);

// 批量读取
buffer.get(data);

// 批量写入指定位置
buffer.put(data, 0, 50);

// 批量读取到指定位置
buffer.get(data, 0, 50);

3. 缓冲区比较

1
2
3
4
5
// 比较两个缓冲区
int result = buffer1.compareTo(buffer2);

// 检查缓冲区是否相等
boolean equal = buffer1.equals(buffer2);

总结

Java NIO 缓冲区是 NIO 操作的基础,掌握缓冲区的使用对于理解 NIO 至关重要。本文介绍了缓冲区的核心属性、类型、基本操作和最佳实践,希望能帮助读者更好地理解和使用 Java NIO 缓冲区。

在后续文章中,我们将继续介绍 Java NIO 的通道(Channel)和选择器(Selector),敬请期待!