这个(无锁)队列实现线程安全吗?

时间:2009-10-27 23:44:09

标签: java multithreading locking thread-safety

我正在尝试用Java创建一个无锁队列实现,主要用于个人学习。队列应该是一般的,允许任意数量的读者和/或作者同时使用。

请您查看一下,并提出您发现的任何改进/问题吗?

谢谢。

import java.util.concurrent.atomic.AtomicReference;

public class LockFreeQueue<T> {
    private static class Node<E> {
        E value;
        volatile Node<E> next;

        Node(E value) {
            this.value = value;
        }
    }

    private AtomicReference<Node<T>> head, tail;

    public LockFreeQueue() {
        // have both head and tail point to a dummy node
        Node<T> dummyNode = new Node<T>(null);
        head = new AtomicReference<Node<T>>(dummyNode);
        tail = new AtomicReference<Node<T>>(dummyNode);
    }

    /**
     * Puts an object at the end of the queue.
     */
    public void putObject(T value) {
        Node<T> newNode = new Node<T>(value);
        Node<T> prevTailNode = tail.getAndSet(newNode);
        prevTailNode.next = newNode;
    }

    /**
     * Gets an object from the beginning of the queue. The object is removed
     * from the queue. If there are no objects in the queue, returns null.
     */
    public T getObject() {
        Node<T> headNode, valueNode;

        // move head node to the next node using atomic semantics
        // as long as next node is not null
        do {
            headNode = head.get();
            valueNode = headNode.next;
            // try until the whole loop executes pseudo-atomically
            // (i.e. unaffected by modifications done by other threads)
        } while (valueNode != null && !head.compareAndSet(headNode, valueNode));

        T value = (valueNode != null ? valueNode.value : null);

        // release the value pointed to by head, keeping the head node dummy
        if (valueNode != null)
            valueNode.value = null;

        return value;
}

4 个答案:

答案 0 :(得分:4)

代码不是线程安全的。考虑putObject(...)

public void putObject(T value) {
    Node<T> newNode = new Node<T>(value);
    Node<T> prevTailNode = tail.getAndSet(newNode);
    prevTailNode.next = newNode;
}

第二个语句在上一个节点的next指针设置之前添加新节点。这只发生在第三个声明中。因此,有一个窗口,其中nextnull;即竞争条件。

即使你解决了这个问题,也存在一个更加阴险的问题。读取Node对象的next字段的线程不会必然看到第二个线程刚写入的值。这是Java内存模型的结果。在这种情况下,确保以下读取总是看到先前写入的值的方法是:

  • next声明为volatile
  • 在同一个对象上的原始互斥体中进行读写操作。

编辑:在更详细地阅读getObject()putObject()的代码时,我可以看到没有任何内容强制将next的非空值刷新到putObject的内存中1}},并没有强迫getObject从主内存中读取next。因此,getObject代码可能会看到next的错误值,导致它在队列中确实存在元素时返回null

答案 1 :(得分:1)

我发现您的代码只有两个问题:

  • 一个是内存操作排序提到Stephen C的问题(可以通过声明nextvalue volatile来解决(节点:值有同样的问题)< / p>

  • 第二个是更微妙的,而不是并发相关:在getObject中返回一个对象之后,你仍然保留其头部引用。这可能会导致内存泄漏。

否则算法没问题。一个模糊的演示(假设以上是固定的):

L1:永远不能从队列中删除tail。这是因为当tail中存储了某些内容时,它会next == null。此外,当您为xxx.next分配内容时(仅在putObject中),它不能是tail,因为getAndSet的原子性以及易失性写入和后续读取之间的顺序 - 假设您读取了非空tail.next,此值必须由putObject写入,因此最后一行之后发生。这意味着发生在上一行之后,这意味着我们正在阅读的值不是来自tail

结果是putObject中的每个对象最终都可以从head到达。那是因为我们在tail之后连接,并且只有在我们将新节点的引用写入其next之后才能删除此节点,这意味着可以从head访问新节点。

getAndSet中的putObject操作会轻易地对添加的对象进行排序。

根据compareAndSet中的成功getObject操作排序出列的对象。

getObject / putObject根据写入/读取到易失性字段next进行排序。

答案 2 :(得分:1)

如果你打电话

,我相信当你试图“释放价值......”时你会得到一个NPE
new LockFreeQueue<?>().getObject();

因为你没有在valueNode对{{1}}进行任何无效检查,尽管上面都是防范它。

答案 3 :(得分:1)

您应该看一下java.util.concurrent.ConcurrentLinkedQueue的实现 http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html 它几乎完成了你想要实现的目标