Implementierung der Thread-sicheren LIFO-Datenstruktur

Thread-sichere LIFO-Datenstrukturimplementierungen

1. Einführung

In diesem Tutorial werdenwe’ll discuss various options for Thread-safe LIFO Data structure implementations.

In der LIFO-Datenstruktur werden Elemente nach dem Last-In-First-Out-Prinzip eingefügt und abgerufen. Dies bedeutet, dass das zuletzt eingefügte Element zuerst abgerufen wird.

In der Informatik iststack der Begriff, der verwendet wird, um sich auf eine solche Datenstruktur zu beziehen.

Astack ist praktisch, um einige interessante Probleme wie die Auswertung von Ausdrücken, das Implementieren von Rückgängig-Operationen usw. zu lösen. Da es in Umgebungen mit gleichzeitiger Ausführung verwendet werden kann, müssen wir es möglicherweise threadsicher machen.

2. Stacks verstehen

Grundsätzlich ist einStack must implement the following methods:

  1. push() - Fügen Sie oben ein Element hinzu

  2. pop() - holt das oberste Element und entfernt es

  3. peek() - Ruft das Element ab, ohne es aus dem zugrunde liegenden Container zu entfernen

Nehmen wir an, wir möchten eine Befehlsverarbeitungs-Engine.

In diesem System ist das Rückgängigmachen von ausgeführten Befehlen ein wichtiges Merkmal.

Im Allgemeinen werden alle Befehle auf den Stack übertragen, und anschließend kann die Undo-Operation einfach implementiert werden:

  • pop() Methode zum Abrufen des zuletzt ausgeführten Befehls

  • Rufen Sie die Methodeundo() für das Befehlsobjekt popped auf

3. Grundlegendes zur Thread-Sicherheit inStacks

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

Kurz gesagt, Rennbedingungen treten auf, wenn die korrekte Ausführung von Code vom Timing und der Reihenfolge der Threads abhängt. Dies geschieht hauptsächlich dann, wenn mehrere Threads die Datenstruktur gemeinsam nutzen und diese Struktur nicht für diesen Zweck ausgelegt ist.

Untersuchen wir eine der folgenden Methoden aus einer Java Collection-Klasse,ArrayDeque:

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;
}

Um den möglichen Race-Zustand im obigen Code zu erklären, nehmen wir zwei Threads an, die diesen Code wie folgt ausführen:

  • Erster Thread führt die dritte Zeile aus: Setzt das Ergebnisobjekt mit dem Element am Index 'head'

  • Der zweite Thread führt die dritte Zeile aus: Setzt das Ergebnisobjekt mit dem Element am Index 'head'

  • Erster Thread führt die fünfte Zeile aus: Setzt den Index "head" auf das nächste Element im Backing-Array zurück

  • Der zweite Thread führt die fünfte Zeile aus: Setzt den Index "head" auf das nächste Element im Backing-Array zurück

Hoppla! Jetzt würden beide Ausführungen dasselbe Ergebnisobjekt zurückgeben

Um solche Race-Bedingungen zu vermeiden, sollte in diesem Fall ein Thread die erste Zeile erst ausführen, wenn der andere Thread den "Kopf" -Index in der fünften Zeile zurückgesetzt hat. Mit anderen Worten, der Zugriff auf das Element unter dem Index "head" und das Zurücksetzen des Index "head" sollte für einen Thread atomar erfolgen.

In diesem Fall hängt die korrekte Ausführung von Code natürlich vom Timing der Threads ab und ist daher nicht threadsicher.

4. Thread-sichere Stacks mit Locks

In diesem Abschnitt werden zwei mögliche Optionen für konkrete Implementierungen eines thread-sicherenstack.  erläutert

Insbesondere werden wir den JavaStack and und einen fadensicheren dekoriertenArrayDeque.  abdecken

Beide verwendenLocks für den sich gegenseitig ausschließenden Zugriff.

4.1. Verwenden der JavaStack

Java Collections verfügt über eine Legacy-Implementierung für threadsichereStack, die aufVector basiert und im Grunde eine synchronisierte Variante vonArrayList. ist

Das offizielle Dokument selbst schlägt jedoch vor, die Verwendung vonArrayDeque in Betracht zu ziehen. Daher werden wir nicht zu sehr ins Detail gehen.

Obwohl JavaStack threadsicher und unkompliziert zu verwenden ist, weist diese Klasse große Nachteile auf:

  • Das Einstellen der Anfangskapazität wird nicht unterstützt

  • Es verwendet Sperren für alle Vorgänge. Dies kann die Leistung von Single-Thread-Ausführungen beeinträchtigen.

4.2. Verwenden vonArrayDeque

Using the Deque interface is the most convenient approach for LIFO data structures as it provides all the needed stack operations.ArrayDeque ist eine solche konkrete Implementierung.  

Da für die Vorgänge keine Sperren verwendet werden, funktionieren Single-Threaded-Ausführungen einwandfrei. Bei Multi-Thread-Ausführungen ist dies jedoch problematisch.

Wir können jedoch einen Synchronisationsdekorator fürArrayDeque. implementieren. Obwohl dies ähnlich wie dieStack-Klasse des Java Collection Framework funktioniert, ist das wichtige Problem derStack-Klasse, das Fehlen der anfänglichen Kapazitätseinstellung, gelöst.

Werfen wir einen Blick auf diese Klasse:

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();
    }
}

Beachten Sie, dass unsere Lösung der Einfachheit halber nichtDeque selbst implementiert, da sie viel mehr Methoden enthält.

Außerdem enthält GuaveSynchronizedDeque , was eine produktionsbereite Implementierung eines dekoriertenArrayDequeue.ist

5. Verriegelungsfreie, fadensichere Stapel

ConcurrentLinkedDeque ist eine sperrfreie Implementierung derDeque-Schnittstelle. This implementation is completely thread-safe, daefficient lock-free algorithm. verwendet wird

Im Gegensatz zu sperrenbasierten Implementierungen sind sperrenfreie Implementierungen gegen die folgenden Probleme immun.

  • Priority inversion - Dies tritt auf, wenn der Thread mit niedriger Priorität die Sperre enthält, die von einem Thread mit hoher Priorität benötigt wird. Dies kann dazu führen, dass der Thread mit hoher Priorität blockiert wird

  • Deadlocks - Dies tritt auf, wenn verschiedene Threads denselben Ressourcensatz in einer anderen Reihenfolge sperren.

Darüber hinaus bieten sperrenfreie Implementierungen einige Funktionen, mit denen sie sowohl in Umgebungen mit einem als auch mit mehreren Threads perfekt eingesetzt werden können.

  • Für nicht gemeinsam genutzte Datenstrukturen und fürsingle-threaded access, performance would be at par with ArrayDeque

  • Für gemeinsam genutzte Datenstrukturen Leistungvaries according to the number of threads that access it simultaneously.

In Bezug auf die Benutzerfreundlichkeit unterscheidet es sich nicht vonArrayDeque, da beide dieDeque-Schnittstelle implementieren.

6. Fazit

In diesem Artikel haben wir diestack data-Struktur und ihre Vorteile beim Entwerfen von Systemen wie der Command Processing Engine und Expression Evaluators erläutert.

Darüber hinaus haben wir verschiedene Stack-Implementierungen im Java-Auflistungsframework analysiert und ihre Leistungs- und Threadsicherheitsnuancen erörtert.

Wie üblich finden sich Codebeispiele inover on GitHub.