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是线程安全的,并且是无锁的,具有较高的性能。