Является ли эта (без блокировки) реализация очереди поточной безопасностью?

Я пытаюсь создать реализацию очереди без блокировки в 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;
}

Ответ 1

Код не является потокобезопасным. Рассмотрим putObject(...):

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

Второй оператор добавляет новый node до того, как был установлен указатель node next. Это происходит только в третьем заявлении. Таким образом, есть окно, в котором next есть null; т.е. состояние гонки.

Даже если вы исправили это, есть более коварная проблема. Поток, читающий поле next для объекта node, не обязательно будет видеть значение, которое только что написал второй поток. Это следствие модели памяти Java. В этом случае способ убедиться, что следующее чтение всегда видит ранее записанное значение, равно либо:

  • объявить next равным volatile или
  • выполняют как чтение, так и запись в примитивном мьютексе на одном и том же объекте.

EDIT: более подробно прочитав код для getObject() и putObject(), я вижу, что ничто не заставляет ненулевое значение next быть сброшенным в память в putObject, и ничто не заставляет getObject, чтобы читать next из основной памяти. Таким образом, код getObject мог видеть неправильное значение next, заставляя его возвращать null, когда на самом деле есть элемент в очереди.

Ответ 2

Я вижу только две проблемы с вашим кодом:

  • один из них - проблема с операциями памяти, упорядочивая упомянутый Стивен C (можно решить, объявив next и value volatile) (Node: значение имеет ту же проблему)

  • вторая более тонкая, а не concurrency, связанная: после того, как вы вернете объект в getObject, вы по-прежнему сохраняете свою ссылку с головы. Это может привести к утечке памяти.

В противном случае алгоритм в порядке. Смутная демонстрация (предположим, что это исправлено):

L1: tail никогда нельзя удалить из очереди. Это происходит потому, что, когда что-то хранится в tail, оно имеет next == null. Кроме того, когда вы присваиваете что-то xxx.next (только в putObject), оно не может быть tail, из-за атомарности getAndSet и упорядочения между волатильной записью и последующим чтением - предполагается, что вы читаете ненулевой tail.next, это значение должно быть записано putObject и, следовательно, происходит после последней строки. Это означает, что это происходит - после предыдущей строки, что означает, что значение, которое мы читаем, не от tail.

Следствием этого является то, что каждый объект в putObject в конечном итоге будет доступен из head. Это потому, что мы подключаемся после tail, и этот node можно удалить только после того, как мы напишем новую ссылку node на ее next, что означает, что новый node доступен из head.

Добавленные объекты тривиально упорядочены операцией getAndSet в putObject.

Выделенные объекты упорядочены в соответствии с успешными операциями compareAndSet в getObject.

getObject/putObject упорядочиваются согласно записи/чтению в поле volatile next.

Ответ 3

Я считаю, что вы получите NPE, когда пытаетесь "освободить значение...", если вы вызываете

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

так как вы не выполняете никаких проверок недействительности на valueNode там, несмотря на то, что он защищен от него выше.