【JUC进阶】12. 环形缓冲区

目录

1、前言

2、基本概述

2.1、什么是环形缓冲区

2.2、结构刨析

2.3、优点

2.4、缺点

3、如何使用

3.1、定义一个环形缓冲区

3.2、Demo使用


扫描二维码关注公众号,回复: 15720366 查看本文章

1、前言

上一篇《【JUC进阶】11. BlockingQueue》中介绍到ArrayBlockingQueue,在物理上是一个数组,但在逻辑上来说是个环形结构。这就衍生出来我们今天要介绍的主题,环形缓冲区。

2、基本概述

2.1、什么是环形缓冲区

环形缓冲区(Circular Buffer)是一种数据结构,它允许我们在固定大小的缓冲区中高效地存储和读取数据。这种缓冲区通常用于处理流式数据,例如网络数据流或文件数据流。

他之所以被称为环形缓冲区,因为它循环存储数据。数据以 FIFO(先进先出)方式从缓冲区读取,这意味着首先读取最旧的数据。我们使用缓冲区在两点(例如生产者和消费者)之间存储和传输数据。

其大致结构如图:

循环缓冲区有一个指针指向缓冲区的下一个空位置,并且我们随着每个新条目递增该指针。这意味着当缓冲区已满时,我们添加一个新元素,它会覆盖最旧的元素。这可以确保缓冲区不会溢出,并且新数据不会覆盖重要数据。当缓冲区已满时,循环缓冲区不需要移动元素来为新数据腾出空间。

相反,当缓冲区已满时,新数据将覆盖最旧的数据。将元素添加到循环缓冲区的时间复杂度是常数 O(1)。这使得它在我们必须快速添加和删除数据的实时系统中非常高效。

2.2、结构刨析

循环缓冲区有两个指针,一个指向缓冲区的头部(head),另一个指向缓冲区的尾部(tail)。头指针指向我们将插入下一个元素的位置,尾指针指向缓冲区中最旧元素的位置。

当头指针和尾指针相遇时,我们认为缓冲区已满。实现循环缓冲区的一种方法是使用带有模运算符的数组,当到达数组末尾时进行回绕:

2.3、优点

  1. 节省内存:环形缓冲区可以循环使用,因此不需要一直分配固定大小的内存空间。当缓冲区已满时,新的数据将覆盖最早的数据,从而减少了内存的占用。这对于处理大量数据或者有限的内存资源非常重要。
  2. 高性能:环形缓冲区可以提高数据读取和写入的效率。由于数据在缓冲区中是循环存储的,读/写指针只需要不断移动,而不需要频繁地分配和释放内存。这使得环形缓冲区非常适合处理高速数据流,例如网络传输或实时数据处理。
  3. 适用于并发场景:环形缓冲区可以支持多个读者和写者同时访问。当多个线程需要同时读取或写入数据时,可以通过互斥锁或其他同步机制来确保数据的正确性和一致性。这使得环形缓冲区非常适合并发处理和多线程编程。

2.4、缺点

  1. 数据覆盖:当缓冲区已满时,新的数据将覆盖最早的数据,这可能导致数据丢失或重要信息被覆盖。在某些应用场景下,这种数据覆盖可能会导致问题,需要特别注意。
  2. 数据不一致:由于环形缓冲区的特性,数据的读取和写入是循环进行的,这可能会导致数据的不一致性。例如,当多个线程同时读取和写入数据时,可能会出现数据冲突或数据错乱的情况。
  3. 难以扩展:环形缓冲区的容量是固定的,无法动态扩展。当缓冲区已满时,如果需要处理更多的数据,必须重新分配更大的内存空间,这可能会导致性能下降或内存占用增加的问题。
  4. 指针管理复杂:由于环形缓冲区的特殊性质,读/写指针需要特殊管理,以确保数据的正确性和一致性。这可能会增加代码的复杂度,并引入潜在的错误风险。
  5. 并发控制开销:在多线程环境下,环形缓冲区需要使用同步机制(如互斥锁)来保护数据的读取和写入操作。这可能会导致并发控制开销增加,并可能降低系统的性能。

3、如何使用

3.1、定义一个环形缓冲区

/**
 * @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;
    }
}

3.2、Demo使用

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。且多次读取,循环缓冲区是重复使用的。

猜你喜欢

转载自blog.csdn.net/p793049488/article/details/131693994