无锁队列:为什么读取`Atomic*`两次?
我正在阅读The Art of Multiprocessor Programming, 2ed并且我注意到以下模式用于读取多个Atomic*字段:
while (true) {
var v1 = atomic1.get();
var v2 = atomic2.get();
...
var vN = atomicN.get();
if (v1 == atomic1.get()) {
// do work
}
}
这个建设的目的是什么?
我在书中找到的唯一解释是:
... 检查读取的值是否一致 ...
我不明白这个解释。
这是LockFreeQueue,它使用了书中的这种模式:
public class LockFreeQueue<T> {
AtomicReference<Node> head, tail;
public LockFreeQueue() {
Node node = new Node(null);
head = new AtomicReference(node);
tail = new AtomicReference(node);
}
public void enq(T value) {
Node node = new Node(value);
while (true) {
Node last = tail.get();
Node next = last.next.get();
if (last == tail.get()) { // <=== WHY: reading tail.get() again
if (next == null) {
if (last.next.compareAndSet(next, node)) {
tail.compareAndSet(last, node);
return;
}
} else {
tail.compareAndSet(last, next);
}
}
}
}
public T deq() throws EmptyException {
while (true) {
Node first = head.get();
Node last = tail.get();
Node next = first.next.get();
if (first == head.get()) { // <=== WHY: reading head.get() again
if (first == last) {
if (next == null) {
throw new EmptyException();
}
tail.compareAndSet(last, next);
} else {
T value = next.value;
if (head.compareAndSet(first, next))
return value;
}
}
}
}
}
public class Node {
public T value;
public AtomicReference<Node> next;
public Node(T value) {
this.value = value;
next = new AtomicReference<Node>(null);
}
}
我在 SO 上看到了另一个类似的问题:无锁队列算法,重复读取以保持一致性。
但:
- 接受的答案有负分,并表示所有答案都可以在不重复阅读的情况下工作,但不提供任何证明
- 它讨论了一种不同的算法:该算法显式释放节点,而这本书主要是关于 java 中的算法(其中节点由 GC 隐式释放)。
UPD:这本书说这LockFreeQueue是Maged Michael 和 Michael Scott 的队列算法的稍微简化版本。
这与上面提到的类似 SO 问题中讨论的算法相同。
回答
我认为一般的想法是作者将按照给定的顺序更新字段,并且每次“更新”时第一个字段的值总是会改变。因此,如果读取器在第二次读取时看到第一个字段没有更改,则它知道它已读取所有字段的一组一致的值(快照)。