无锁队列中哪里有错误?

时间:2013-12-20 13:33:28

标签: java queue volatile lock-free

我写了一个Java锁定免费队列实现。它有一个并发错误。我找不到它了。这段代码并不重要。我只是担心我无法解释与volatile变量相关的观察行为。

异常可见错误(“null head”)。这是不可能的状态,因为存在保持当前队列大小的原子整数。队列有一个存根元素。它规定读者线程不会改变尾指针,而写入线程不会改变头指针。

队列长度变量保证链表永远不会为空。它是一个信号量。

take方法的行为类似于被盗的长度值。

class Node<T> {
    final AtomicReference<Node<T>> next = new AtomicReference<Node<T>>();
    final T ref;
    Node(T ref) {
        this.ref = ref;
    }
}
public class LockFreeQueue<T> {
    private final AtomicInteger length = new AtomicInteger(1);
    private final Node stub = new Node(null);
    private final AtomicReference<Node<T>> head = new AtomicReference<Node<T>>(stub);
    private final AtomicReference<Node<T>> tail = new AtomicReference<Node<T>>(stub);

    public void add(T x) {
        addNode(new Node<T>(x));
        length.incrementAndGet();
    }

    public T takeOrNull() {
        while (true) {
            int l = length.get();
            if (l == 1) {
                return null;
            }
            if (length.compareAndSet(l, l - 1)) {
                break;
            }
        }
        while (true) {
            Node<T> r = head.get();
            if (r == null) {
                throw new IllegalStateException("null head");
            }
            if (head.compareAndSet(r, r.next.get())) {
                if (r == stub) {
                    stub.next.set(null);
                    addNode(stub);
                } else {
                    return r.ref;
                }
            }
        }
    }

    private void addNode(Node<T> n) {
        Node<T> t;
        while (true) {
            t = tail.get();
            if (tail.compareAndSet(t, n)) {
                break;    
            }
        }
        if (t.next.compareAndSet(null, n)) {
            return;
        }
        throw new IllegalStateException("bad tail next");
    }
}

1 个答案:

答案 0 :(得分:1)

我认为在takeOrNull()中如何使用计数器时出错,当你删除存根时,你将Length减少1,但是在最后添加存根时不要重新增加它,因为你使用addNode()而不是add()。 假设您已成功添加元素,因此您的队列如下所示:

Length is 2
STUB -> FIRST_NODE -> NULL
 ^          ^
 |          |
Head       Tail

所以现在一个线程开始执行takeOrNull(),length减少到1,Head移动到FIRST_NODE,因为这是STUB节点,它会重新添加到结尾,所以现在你有:

Length is 1
FIRST_NODE -> STUB -> NULL
 ^             ^
 |             |
Head          Tail
你知道吗?长度现在是1!在下一个takeOrNull()中,即使FIRST_NODE仍然在队列中并且从未返回过,您将获得NULL ...您只是(暂时)丢失了一段数据。 此外,您现在可以无限重复此广告并开始累积节点。 就像添加三个节点一样,长度为4,你有FIRST,STUB,NEW1,NEW2,NEW3。如果你然后做三个takeOrNull(),你最终会得到NEW2,NEW3,STUB和Length 1。 所以这样你最终会失去元素,但我承认不完全确定这会如何触发异常。让我吃点再考虑一下。 ; - )

编辑:好的食物对我有好处,我想出了一个触发头部空例外的序列。 让我们从一个有一个元素的有效队列开始:

Length is 2
STUB -> FIRST_NODE -> NULL
 ^          ^
 |          |
Head       Tail

现在我们有四个线程,两个尝试takeOrNull(),两个同时添加()。 两个添加线程都正确地移动了尾部指针,第一个移动尾部从FIRST到SECOND,然后暂停。第二个添加线程然后从SECOND移动到第三个,然后更新旧尾部(SECOND的)下一个指针,然后递增计数器并退出。 我们离开了:

Length is 3
STUB -> FIRST_NODE -> NULL          SECOND_NODE ->  THIRD_NODE -> NULL
 ^                                                     ^
 |                                                     |
Head                                                  Tail

现在两个takeOrNull线程被唤醒并执行,因为Length是3,两者都可以获得一个元素!第一个将Head从STUB移动到FIRST,第二个将Head从FIRST移动到NULL。现在HEAD为null,每当下次调用takeOrNull()时,EXCEPTION!