Потокобезопасные реализации структуры данных LIFO

Потокобезопасные реализации структуры данных LIFO

1. Вступление

В этом руководствеwe’ll discuss various options for Thread-safe LIFO Data structure implementations.

В структуре данных LIFO элементы вставляются и извлекаются в соответствии с принципом «Последний пришел первым». Это означает, что последний вставленный элемент извлекается первым.

В информатикеstack - это термин, используемый для обозначения такой структуры данных.

stack удобен для решения некоторых интересных проблем, таких как вычисление выражений, выполнение операций отмены и т. Д. Поскольку его можно использовать в средах параллельного выполнения, нам может потребоваться сделать его поточно-ориентированным.

2. ПониманиеStacks

В основном aStack must implement the following methods:

  1. push() - добавить элемент вверху

  2. pop() - получить и удалить верхний элемент

  3. peek() - получить элемент без удаления из нижележащего контейнера

Как обсуждалось ранее, предположим, что нам нужен механизм обработки команд.

В этой системе отмена выполненных команд является важной особенностью.

В общем, все команды помещаются в стек, а затем можно просто выполнить операцию отмены:

  • pop() метод для получения последней выполненной команды

  • вызвать методundo() для всплывающего объекта команды

3. Понимание безопасности потоков вStacks

If a data structure is not thread-safe, when accessed concurrently, it might end up having race conditions.

В двух словах, условия гонки возникают, когда правильное выполнение кода зависит от времени и последовательности потоков. Это происходит главным образом, если структура данных совместно используется несколькими потоками, и эта структура не предназначена для этой цели.

Давайте рассмотрим нижеприведенный метод из класса Java CollectionArrayDeque:

public E pollFirst() {
    int h = head;
    E result = (E) elements[h];
    // ... other book-keeping operations removed, for simplicity
    head = (h + 1) & (elements.length - 1);
    return result;
}

Чтобы объяснить состояние потенциальной гонки в приведенном выше коде, давайте предположим, что два потока исполняют этот код, как показано в следующей последовательности:

  • Первый поток выполняет третью строку: устанавливает объект результата с элементом с индексом «head»

  • Второй поток выполняет третью строку: устанавливает объект результата с элементом с индексом «head»

  • Первый поток выполняет пятую строку: сбрасывает индекс «head» к следующему элементу в массиве поддержки

  • Второй поток выполняет пятую строку: сбрасывает индекс «head» к следующему элементу в массиве поддержки

К сожалению! Теперь оба выполнения вернут один и тот же объект результата

Чтобы избежать таких состояний гонки, в этом случае поток не должен выполнять первую строку, пока другой поток не завершит сброс индекса «головы» в пятой строке. Другими словами, доступ к элементу в индексе «head» и сброс индекса «head» должен происходить атомно для потока.

Очевидно, что в этом случае правильное выполнение кода зависит от синхронизации потоков и, следовательно, не является потокобезопасным.

4. Многопоточные стеки с использованием замков

В этом разделе мы обсудим два возможных варианта конкретной реализации поточно-ориентированногоstack. 

В частности, мы рассмотрим JavaStack и поточно декорируемArrayDeque. .

Оба используютLocks для взаимоисключающего доступа.

4.1. Использование JavaStack

Коллекции Java имеют устаревшую реализацию для потокобезопасногоStack, основанную наVector, которая в основном является синхронизированным вариантомArrayList.

Однако в официальном документе предлагается рассмотреть возможность использованияArrayDeque. Поэтому не будем вдаваться в подробности.

Хотя JavaStack является поточно-ориентированным и простым в использовании, у этого класса есть серьезные недостатки:

  • У него нет поддержки для установки начальной емкости

  • Он использует блокировки для всех операций. Это может снизить производительность для однопоточных исполнений.

4.2. ИспользуяArrayDeque

Using the Deque interface is the most convenient approach for LIFO data structures as it provides all the needed stack operations.ArrayDeque - одна из таких конкретных реализаций.  

Поскольку он не использует блокировки для операций, однопоточное выполнение будет работать нормально. Но для многопоточных исполнений это проблематично.

Тем не менее, мы можем реализовать декоратор синхронизации дляArrayDeque.. Хотя это работает аналогично классуStack Java Collection Framework, важная проблема классаStack, отсутствие начальной настройки емкости, решена.

Давайте посмотрим на этот класс:

public class DequeBasedSynchronizedStack {

    // Internal Deque which gets decorated for synchronization.
    private ArrayDeque dequeStore;

    public DequeBasedSynchronizedStack(int initialCapacity) {
        this.dequeStore = new ArrayDeque<>(initialCapacity);
    }

    public DequeBasedSynchronizedStack() {
        dequeStore = new ArrayDeque<>();
    }

    public synchronized T pop() {
        return this.dequeStore.pop();
    }

    public synchronized void push(T element) {
        this.dequeStore.push(element);
    }

    public synchronized T peek() {
        return this.dequeStore.peek();
    }

    public synchronized int size() {
        return this.dequeStore.size();
    }
}

Обратите внимание, что наше решение не реализуетDeque для простоты, поскольку оно содержит гораздо больше методов.

Кроме того, Guava содержитSynchronizedDeque w, который представляет собой готовую к производству реализацию декорированногоArrayDequeue.

5. Беззащитные многопоточные стеки

ConcurrentLinkedDeque - это реализация интерфейсаDeque без блокировки. This implementation is completely thread-safe, поскольку он используетefficient lock-free algorithm.

Реализации без блокировок защищены от следующих проблем, в отличие от основанных на блокировке.

  • Priority inversion - это происходит, когда поток с низким приоритетом удерживает блокировку, необходимую для потока с высоким приоритетом. Это может привести к блокировке потока с высоким приоритетом

  • Deadlocks - это происходит, когда разные потоки блокируют один и тот же набор ресурсов в разном порядке.

Вдобавок к этому реализации без блокировок имеют некоторые особенности, которые делают их идеальными для использования как в однопоточной, так и в многопоточной среде.

  • Для неразделенных структур данных и дляsingle-threaded access, performance would be at par with ArrayDeque

  • Для общих структур данных производительностьvaries according to the number of threads that access it simultaneously.

А с точки зрения удобства использования он ничем не отличается отArrayDeque, поскольку оба реализуют интерфейсDeque.

6. Заключение

В этой статье мы обсудили структуруstack data и ее преимущества при разработке таких систем, как механизм обработки команд и оценщики выражений.

Кроме того, мы проанализировали различные реализации стека в структуре коллекций Java и обсудили их нюансы производительности и безопасности потоков.

Как обычно, примеры кода можно найтиover on GitHub.