【问题标题】:Is this (Lock-Free) Queue Implementation Thread-Safe?这是(无锁)队列实现线程安全的吗?
【发布时间】:2010-12-10 16:48:05
【问题描述】:

我正在尝试用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;
}

【问题讨论】:

标签: java multithreading locking thread-safety


【解决方案1】:

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

【讨论】:

  • 谢谢。我会检查它以获取更多想法。但是ConcurrentLinkedQueue太复杂了,因为它支持很多方法,而我的队列要简单得多,这让我可以做更多的假设,尝试更多的优化。
  • 有一种说法是无锁代码本质上是复杂的。 ;)
【解决方案2】:

我相信当你尝试“释放价值......”时,如果你调用,你会得到一个 NPE

new LockFreeQueue<?>().getObject();

因为您没有对那里的valueNode 执行任何无效检查,尽管上面有所防范。

【讨论】:

  • 你是对的。我完全错过了!我现在修好了。谢谢!
【解决方案3】:

代码不是线程安全的。考虑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 中的内存中,并且没有任何东西强制@ 987654333@ 从主存读取next。所以getObject 代码可能会看到错误的next 值,导致它在队列中确实有一个元素时返回null

【讨论】:

  • 对不起,你错了 next 为空。实际上,tail.next 始终为 null,并且没有任何问题。如果 getObject 中的 next 为 null,则循环终止并返回 null(队列为空)。此外,只有在另一个线程成功写入 head 时,getObject 中的循环才能旋转 - 例如。另一个线程成功了,这就是我们想要的无锁算法。
  • 抱歉,但在许多方面仍然不正确(例如,整个 EDIT2 表明您没有注意到队列始终包含至少一个虚拟节点)。
  • 感谢您的帮助。我更新了我的代码以使 next 易失,并在 getObject() 中将 nextNode 重命名为 valueNode 以使其目的更清晰。但我猜想在阅读器线程中读取一个陈旧的值不会使代码“不是线程安全的”;这只是一个性能问题,对吧?
  • @Hosam - 不。我认为当队列中确实有一个元素时,getObject() 可以返回null。事实上,它可以反复做。
  • @Stephen C:是的,它可以,但这只会延迟该元素的处理。我想这取决于我们如何看待它,队列中的项目的可用性是立即需要还是可以稍后接收。反正我已经修好了(我想),所以谢谢你指出来。
【解决方案4】:

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

  • 一个是内存操作顺序Stephen C提到的问题(可以通过声明nextvaluevolatile来解决)(Node:value也有同样的问题)

  • second 是一个更微妙的,与并发无关:在 getObject 返回一个对象后,您仍然保留它来自 head 的引用。这可能会导致内存泄漏。

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

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

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

添加的对象通过putObject 中的getAndSet 操作进行简单排序。

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

getObject/putObject 是根据对 volatile 字段next 的写入/读取排序的。

【讨论】:

  • 你的意思是“你仍然保留它的头部引用”? head 在该方法中设置为 nextNode
  • 我的意思是,nextNode 中的value 通过head 返回并保留。如果不再从队列中取出元素(例如队列之后为空),则对返回值的引用永远不会被释放。
  • 谢谢!我已经修复了内存泄漏问题,并使next volatile。但我还不明白为什么value 需要不稳定。能否进一步解释一下?
猜你喜欢
  • 1970-01-01
  • 2013-07-24
  • 2023-03-18
  • 1970-01-01
  • 2014-02-28
  • 1970-01-01
  • 1970-01-01
  • 2021-05-12
  • 1970-01-01
相关资源
最近更新 更多