线程安全的无锁RingBuffer

5,229 阅读2分钟

  RingBuffer是环形缓冲区,支持读和写两种操作,类似于循环队列。在实现上,一般用数组存储数据,同时设置双指针head和tail,head指向队首,tail指向队尾。读数据时,head++;写数据时,tail++。显然,RingBuffer不是线程安全的,需要对读写数据进行同步。然而,有一种特殊情况:一个线程读,一个线程写,在这种情况下可以实现线程安全的无锁RingBuffer,代码如下:

public class RingBuffer<T> {
    private volatile T[] elements;
    private volatile int head;
    private volatile int tail;

    public RingBuffer(int capacity){
        this.elements=(T[])new Object[capacity];
        this.head=0;
        this.tail=-1;
    }

    private boolean isEmpty(){
        return tail+1==head;
    }

    private boolean isFull(){
        return tail+1-elements.length==head;
    }

    public void push(T element) throws IllegalArgumentException{
        if(isFull())
            throw new IllegalArgumentException("full queue");
        elements[(tail+1)%elements.length]=element;
        tail++;
    }

    public T pop() throws IllegalArgumentException{
        if(isEmpty())
            throw new IllegalArgumentException("empty queue");
        T element=elements[head%elements.length];
        head++;
        return element;
    }
}

  为什么上述代码能够确保线程安全?可以看到,创建RingBuffer后,只有写线程修改tail,只有读线程修改head,并且始终只有一个读线程,一个写线程,因此是没有并发写操作的。然而,由于读操作和写操作都不是原子性的,有可能读操作发生在写操作的过程中,写操作发生在读操作的过程中。这样会导致以下两个问题:

  • 对于读操作,当RingBuffer为空时,有可能读到还没写的数据。
  • 对于写操作,当RingBuffer为满时,有可能写到还没读的数据。

但是,对于写操作,我们是先修改elements,再修改tail;对于读操作,我们是先修改elements,再修改head。因此上述两个问题是不存在的,我们说上述RingBuffer是线程安全的,并且是无锁的,具有较高的性能。