这个(无锁)队列实现线程安全吗?
我正在尝试用Java创建一个无锁队列实现,主要用于个人学习。 队列应该是一般的,允许任意数量的读者和/或作者同时。
您能否回顾一下,并建议您找到的任何改进/问题?
谢谢。
import java.util.concurrent.atomic.AtomicReference; public class LockFreeQueue { private static class Node { E value; volatile Node next; Node(E value) { this.value = value; } } private AtomicReference<Node> head, tail; public LockFreeQueue() { // have both head and tail point to a dummy node Node dummyNode = new Node(null); head = new AtomicReference<Node>(dummyNode); tail = new AtomicReference<Node>(dummyNode); } /** * Puts an object at the end of the queue. */ public void putObject(T value) { Node newNode = new Node(value); Node 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 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 // (ie 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; }
代码不是线程安全的。 考虑putObject(...)
:
public void putObject(T value) { Node newNode = new Node (value); Node prevTailNode = tail.getAndSet(newNode); prevTailNode.next = newNode; }
第二个语句在上一个节点的next
指针设置之前添加新节点。 这只发生在第三个声明中。 因此,有一个窗口,其中next
是null
; 即竞争条件。
即使你解决了这个问题,也存在一个更加阴险的问题。 读取Node对象的next
字段的线程不一定会看到第二个线程刚写入的值。 这是Java内存模型的结果。 在这种情况下, 确保以下读取始终看到较早写入值的方法是:
- 声明
next
是volatile
,或者 - 在同一个对象上的原始互斥体中进行读写操作。
编辑:在更详细地阅读getObject()
和putObject()
的代码时,我可以看到没有任何东西强制将next
的非null值刷新到putObject
内存中,并且没有任何东西强迫getObject
从主内存中读取。 因此, getObject
代码可能会看到next
的错误值,导致它在队列中确实存在元素时返回null
。
我看到你的代码只有两个问题:
-
一个是内存操作排序提到Stephen C的问题(可以通过声明
next
和value
volatile
来解决)(Node:value有同样的问题) -
第二个是更微妙的,而不是并发相关:在
getObject
返回一个对象后,你仍然保留它的头部引用。 这可能会导致内存泄漏。
否则算法没问题。 一个模糊的演示(假设以上是固定的):
L1:永远不能从队列中删除tail
。 这是因为当一些东西存储在tail
,它有next == null
。 此外,当您为xxx.next
(仅在putObject
)分配内容时,它不能是tail
,因为getAndSet的primefaces性以及volatile写入和后续读取之间的顺序 – 假设您读取了非null的tail.next
,此值必须由putObject
编写,因此发生在它的最后一行之后。 这意味着它发生在前一行之后,这意味着我们正在读取的值不是来自tail
。
由此产生的结果是putObject
中的每个对象最终都可以从head
到达。 那是因为我们在tail
之后连接,并且只有在我们将新节点的引用写入其next
节点之后才能删除此节点,这意味着可以从head
访问新节点。
添加的对象由putObject
的getAndSet
操作按顺序排序。
根据getObject
成功compareAndSet
操作对排队的对象进行排序。
getObject
/ putObject
根据写入/读取到volatile字段进行排序。
如果你打电话,我相信当你试图“释放价值……”时你会得到一个NPE
new LockFreeQueue>().getObject();
因为你valueNode
那里的valueNode
执行任何无效检查,尽管上面有防范它。
你应该看一下java.util.concurrent.ConcurrentLinkedQueue的实现http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html它几乎做了什么你正在努力实现