我正在考虑实现一个无锁的循环数组。一个问题是以无锁的方式保持头部和尾部指针。我想到的代码是:
int circularIncrementAndGet(AtomicInteger i) {
i.compareAndSet(array.length - 1, -1);
return i.incrementAndGet();
}
然后我会做类似的事情:
void add(double value) {
int idx = circularIncrementAndGet(tail);
array[idx] = value;
}
(注意,如果数组是完整的旧值将被覆盖,我很好)。
有没有人发现这个设计有问题?我怀疑可能存在一种我没有看到的竞争状态。
答案 0 :(得分:4)
查看disruptor:http://lmax-exchange.github.io/disruptor/,它是Java中的一个开源无锁循环缓冲区。
答案 1 :(得分:4)
更简单的方法是使用2大小的幂并执行以下操作。
final double[] array;
final int sizeMask;
final AtomicInteger i = new AtomicInteger();
public CircularBuffer(int size) {
assert size > 1 && ((size & (size -1)) == 0); // test power of 2.
array = new double[size];
sizeMask = size -1;
}
void add(double value) {
array[i.getAndIncrement() & sizeMask] = value;
}
答案 2 :(得分:2)
是的,有竞争条件。
说i = array.length - 2
,两个主题输入circularIncrementAndGet()
:
Thread 1: i.compareAndSet(array.length - 1, -1) results in i = array.length - 2
Thread 2: i.compareAndSet(array.length - 1, -1) results in i = array.length - 2
Thread 1: i.incrementAndGet() results in i = array.length - 1
Thread 2: i.incrementAndGet() results in i = array.length
当线程2到达ArrayIndexOutOfBoundsException
时(以及随后array[idx] = value
的所有后续调用,直到add()
溢出),才会导致i
。
@Peter Lawrey提出的解决方案没有遇到这个问题。
答案 3 :(得分:1)
如果您坚持以下约束:
可以实现循环数组/队列。
排队线程拥有尾指针。出列线程拥有头指针。除了一个条件,这两个线程到目前为止不共享任何状态,因此没有问题。
这个条件是测试空虚或丰满。
考虑为空表示头==尾;考虑完全意味着tail == head - 1模数组大小。 Enqueue必须检查队列是否已满,dequeue必须检查队列是否为空。你需要在数组中浪费一个索引来检测full和empty之间的区别 - 如果你排入最后一个bucket,那么full将是head == tail
而empty将是head == tail
,现在你就陷入僵局 - 你认为你在同一时间是空虚和充实的,所以没有工作可以完成。
在执行这些检查时,可能会在比较时更新一个值。然而,由于这两个值是单调递增的,因此没有正确性问题:
这是我在Dobb博士多年前发现的实施背后所使用的设计,它对我很有帮助: