上一篇《【JUC进阶】11. BlockingQueue》中介绍到ArrayBlockingQueue,在物理上是一个数组,但在逻辑上来说是个环形结构。这就衍生出来我们今天要介绍的主题,环形缓冲区。
环形缓冲区(Circular Buffer)是一种数据结构,它允许我们在固定大小的缓冲区中高效地存储和读取数据。这种缓冲区通常用于处理流式数据,例如网络数据流或文件数据流。
他之所以被称为环形缓冲区,因为它循环存储数据。数据以 FIFO(先进先出)方式从缓冲区读取,这意味着首先读取最旧的数据。我们使用缓冲区在两点(例如生产者和消费者)之间存储和传输数据。
其大致结构如图:

循环缓冲区有一个指针指向缓冲区的下一个空位置,并且我们随着每个新条目递增该指针。这意味着当缓冲区已满时,我们添加一个新元素,它会覆盖最旧的元素。这可以确保缓冲区不会溢出,并且新数据不会覆盖重要数据。当缓冲区已满时,循环缓冲区不需要移动元素来为新数据腾出空间。
相反,当缓冲区已满时,新数据将覆盖最旧的数据。将元素添加到循环缓冲区的时间复杂度是常数 O(1)。这使得它在我们必须快速添加和删除数据的实时系统中非常高效。
循环缓冲区有两个指针,一个指向缓冲区的头部(head),另一个指向缓冲区的尾部(tail)。头指针指向我们将插入下一个元素的位置,尾指针指向缓冲区中最旧元素的位置。
当头指针和尾指针相遇时,我们认为缓冲区已满。实现循环缓冲区的一种方法是使用带有模运算符的数组,当到达数组末尾时进行回绕:

/**
* @author Shamee loop
* @date 2023/7/11
*/
public class CircularBuffer {
private int[] buffer;
// 头部指针
private int head;
// 尾部指针
private int tail;
private int size;
// 初始容量
private int capacity;
public CircularBuffer(int capacity) {
this.capacity = capacity;
buffer = new int[capacity];
head = 0;
tail = 0;
size = 0;
}
/**
* 向缓冲区中添加数据,如果满了则覆盖
* @param value
*/
public synchronized void push(int value) {
if (size == capacity) {
// 缓冲区已满,覆盖最早的数据
head = (head + 1) % capacity;
}
buffer[tail] = value;
tail = (tail + 1) % capacity;
size++;
}
/**
* 向缓冲区中弹出数据,如果空了,则抛出异常
* @return
*/
public synchronized int pop() {
if (size == 0) {
throw new NoSuchElementException("Buffer is empty");
}
int value = buffer[head];
head = (head + 1) % capacity;
size--;
return value;
}
/**
* 获取缓冲区中第一个数据,但不会弹出数据
* @return
*/
public synchronized int peek() {
if (size == 0) {
throw new NoSuchElementException("Buffer is empty");
}
return buffer[head];
}
/**
* 判断缓冲区是否空了
* @return
*/
public synchronized boolean isEmpty() {
return size == 0;
}
/**
* 判断缓冲区是否满了
* @return
*/
public synchronized boolean isFull() {
return size == capacity;
}
}public static void main(String[] args) {
CircularBuffer buffer = new CircularBuffer(5);
for (int i = 0; i < 10; i++) {
buffer.push(i);
}
for (int i = 0; i < 10; i++) {
int value = buffer.pop();
System.out.println("Received value: " + value);
}
}输出结果:

因为我们定义的容量为5,因此往里面push10个值的时候,后面的新值会把前面的值覆盖,所以我们看到输出结果一直都是5、6、7、8、9。且多次读取,循环缓冲区是重复使用的。