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