我写了一个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");
}
}
答案 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!