使用offer和flush
非阻止并发队列我需要一个无限制的非阻塞并发队列,基本上只有2个操作:
目标是消费者在takeAll上有一个CAS操作,然后可以迭代列表中的元素,而无需每次读取CAS操作。此外,我们已经拥有Node(Entry),因为这需要存储其他一些不可变状态。新节点可以将HEAD作为构造函数参数,创建单向链接列表。
文献中是否存在具有这些特征的队列?
答案 0 :(得分:13)
你走了:
public class FunkyQueue<T> {
private final AtomicReference<Node<T>> _tail = new AtomicReference<Node<T>>();
public void offer(T t) {
while(true) {
Node<T> tail = _tail.get();
Node<T> newTail = new Node<T>(t, tail);
if(_tail.compareAndSet(tail, newTail)) {
break;
}
}
}
public List<T> takeAll() {
Node<T> tail = _tail.getAndSet(null);
LinkedList<T> list = new LinkedList<T>();
while(tail != null) {
list.addFirst(tail.get());
tail = tail.getPrevious();
}
return list;
}
private static final class Node<T>
{
private final T _obj;
private Node<T> _prev;
private Node(T obj, Node<T> prev) {
_obj = obj;
_prev = prev;
}
public T get() {
return _obj;
}
public Node<T> getPrevious() {
return _prev;
}
}
}
答案 1 :(得分:5)
鉴于:nice implementation,offer()
和takeAll()
需要一个CAS。
问题:执行长takeAll()
,因为它需要在相反的方向上完全遍历单链表。
解决方案:在节点上创建其他跳过级别。对于上面提到的数字(N~100K),两个级别就足够了,从而减少takeAll()
到~150的步数。
根据提到的实施,Node
类:
public static final class Node<T> {
private final T value;
private Node<T> prev, prevL1, prevL2;
private Node<T> next, nextL1, nextL2;
private Node(T obj, Node<T> prev, long c) {
value = obj;
this.prev = prev;
// level 1 to skip 64 nodes, level 2 to skip 64^2 nodes
// c is a value from some global addition counter, that
// is not required to be atomic with `offer()`
prevL1 = (c & (64 - 1) == 0) ? prev : prev.prevL1;
prevL2 = (c & (64 * 64 - 1) == 0) ? prev : prev.prevL2;
}
public T get() {
return value;
}
public Node<T> findHead() {
// see below
}
public Node<T> next() {
// see below
}
}
FunkyQueue#offer()
方法:
public void offer(T t) {
long c = counter.incrementAndGet();
for(;;) {
Node<T> oldTail = tail.get();
Node<T> newTail = new Node<T>(t, oldTail, c);
if (tail.compareAndSet(oldTail, newTail))
break;
}
}
FunkyQueue#takeAll()
现在将返回列表的头部:
public Node<T> takeAll() {
return tail.getAndSet(null).findHead();
}
它调用Node#findHead()
,现在可以使用跳过级别来加速向后遍历:
private Node<T> findHead() {
Node<T> n = this;
while (n.prevL2 != null) { // <- traverse back on L2, assigning `next` nodes
n.prevL2.nextL2 = n;
n = n.prevL2;
}
while (n.prevL1 != null) { // <- the same for L1
n.prevL1.nextL1 = n;
n = n.prev1;
}
while (n.prev != null) { // <- the same for L0
n.prev.next = n;
n = n.prev;
}
return n;
}
最后,Node#next()
:
public Node<T> next() {
if (this.next == null && this.nextL1 == null && this.nextL2 == null)
throw new IllegalStateException("No such element");
Node<T> n;
if (this.next == null) { // L0 is not traversed yet
if (this.nextL1 == null) { // the same for L1
n = this.nextL2; // step forward on L2
while (n != this) { // traverse on L1
n.prevL1.nextL1 = n;
n = n.prevL1;
}
}
n = this.nextL1; // step forward on L1
while (n != this) { // traverse on L0
n.prev.next = n;
n = n.prev;
}
}
return this.next;
}
我认为主要观点很明确。应用一些重构,可以使Node#findHead()
因此FunkyQueue#takeAll()
在O(log N)中运行,而在{(1)}中运行Node#next()
。
P.S。如果有人注意到某些错误或错误的语法,请编辑。
答案 2 :(得分:1)
ConcurrentLinkedQueue使用Michael & Scott algorithm,可以进行调整以提供此方法。返回的集合将是已移除的遍历节点的不可修改视图。这看起来像是,
public Collection<E> drain() {
for (;;) {
Node<E> h = head;
Node<E> t = tail;
if (h == t) {
return Collections.emptyList();
} else if (casHead(h, t)) {
return new CollectionView<E>(h, t);
}
}
}
分叉集合并不是很有趣,所以我implemented Mozes & Shavit algorithm为乐观队列(更快的替代方案)。这通过退避竞技场来增强,以结合并发添加以减少由多个生产者引起的争用。