【问题标题】:Where is there bug in lock free queue?无锁队列中的错误在哪里?
【发布时间】:2013-12-20 13:33:28
【问题描述】:

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

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

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

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");
    }
}

【问题讨论】:

  • 在不使用锁定机制的情况下,这段代码如何防止数据竞争?为什么不想使用锁?
  • 什么时候发现问题?您是通过单个阅读器线程获得它还是需要多个阅读器才能看到问题?我怀疑问题出在 takeOrNull 的第二个 while 循环中存在多个读取器线程。
  • 这不是生产代码。把它当作练习。
  • 我测试这个队列 100 个读者和 100 个作者。

标签: java queue volatile lock-free


【解决方案1】:

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

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

所以现在一个线程开始执行 takeOrNull(),长度减少到 1,Head 移动到 FIRST_NODE,由于这是 STUB 节点,它被重新添加到末尾,所以现在你有:

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

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

编辑:好的食物对我有好处,我想出了一个触发 head null 异常的序列。 让我们从一个包含一个元素的有效队列开始:

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

现在我们有四个线程,两个尝试同时进行 takeOrNull() 和两个 add()。 两个添加线程都正确移动了尾部指针,第一个将尾部从 FIRST 移动到 SECOND,然后被暂停。第二个添加线程将尾从 SECOND 移动到 THIRD,然后更新旧尾 (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 为空,每当接下来调用 takeOrNull() 时,异常!

【讨论】:

    猜你喜欢
    • 2017-04-21
    • 2015-06-15
    • 2011-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-07
    • 2013-06-29
    • 1970-01-01
    相关资源
    最近更新 更多