Eine Einführung in atomare Variablen in Java

Eine Einführung in atomare Variablen in Java

1. Einführung

Einfach ausgedrückt, führt der gemeinsam genutzte Status sehr leicht zu Problemen, wenn es sich um Parallelität handelt. Wenn der Zugriff auf gemeinsam genutzte veränderbare Objekte nicht ordnungsgemäß verwaltet wird, kann es bei Anwendungen schnell zu schwer zu erkennenden Fehlern bei der gemeinsamen Nutzung kommen.

In diesem Artikel werden wir die Verwendung von Sperren für den gleichzeitigen Zugriff erneut untersuchen, einige der mit Sperren verbundenen Nachteile untersuchen und schließlich als Alternative atomare Variablen einführen.

2. Schlösser

Werfen wir einen Blick auf die Klasse:

public class Counter {
    int counter;

    public void increment() {
        counter++;
    }
}

In einer Umgebung mit nur einem Thread funktioniert dies einwandfrei. Sobald wir jedoch mehr als einen Thread schreiben lassen, erhalten wir inkonsistente Ergebnisse.

Dies liegt an der einfachen Inkrementierungsoperation (counter++), die wie eine atomare Operation aussehen kann, aber tatsächlich eine Kombination aus drei Operationen ist: Abrufen des Werts, Inkrementieren und Zurückschreiben des aktualisierten Werts.

Wenn zwei Threads gleichzeitig versuchen, den Wert abzurufen und zu aktualisieren, kann dies zu verlorenen Aktualisierungen führen.

Eine Möglichkeit, den Zugriff auf ein Objekt zu verwalten, ist die Verwendung von Sperren. Dies kann erreicht werden, indem das Schlüsselwortsynchronized in der Methodensignaturincrementverwendet wird. Das Schlüsselwortsynchronized stellt sicher, dass immer nur ein Thread gleichzeitig in die Methode eintreten kann (weitere Informationen zum Sperren und Synchronisieren finden Sie unter -Guide to Synchronized Keyword in Java):

public class SafeCounterWithLock {
    private volatile int counter;

    public synchronized void increment() {
        counter++;
    }
}

Darüber hinaus müssen wir das Schlüsselwortvolatilehinzufügen, um eine ordnungsgemäße Referenzsichtbarkeit zwischen den Threads sicherzustellen.

Die Verwendung von Schlössern löst das Problem. Die Leistung nimmt jedoch einen Schlag ab.

Wenn mehrere Threads versuchen, eine Sperre zu erlangen, gewinnt einer von ihnen, während der Rest der Threads entweder blockiert oder angehalten wird.

The process of suspending and then resuming a thread is very expensive und beeinflusst die Gesamteffizienz des Systems.

In einem kleinen Programm, wie z. B.counter, kann die für die Kontextumschaltung aufgewendete Zeit viel länger sein als die tatsächliche Codeausführung, wodurch die Gesamteffizienz erheblich verringert wird.

3. Atomoperationen

Es gibt einen Forschungszweig, der sich darauf konzentriert, nicht blockierende Algorithmen für gleichzeitige Umgebungen zu erstellen. Diese Algorithmen nutzen grundlegende atomare Maschinenbefehle wie Compare-and-Swap (CAS), um die Datenintegrität sicherzustellen.

Eine typische CAS-Operation arbeitet mit drei Operanden:

  1. Der Speicherort, an dem gearbeitet werden soll (M)

  2. Der vorhandene erwartete Wert (A) der Variablen

  3. Der neue Wert (B), der eingestellt werden muss

Die CAS-Operation aktualisiert den Wert in M ​​atomar auf B, jedoch nur, wenn der vorhandene Wert in M ​​mit A übereinstimmt, andernfalls wird keine Aktion ausgeführt.

In beiden Fällen wird der vorhandene Wert in M ​​zurückgegeben. Dies kombiniert drei Schritte - Abrufen des Werts, Vergleichen des Werts und Aktualisieren des Werts - in einer einzelnen Operation auf Maschinenebene.

Wenn mehrere Threads versuchen, denselben Wert über CAS zu aktualisieren, gewinnt einer von ihnen und aktualisiert den Wert. However, unlike in the case of locks, no other thread gets suspended; Stattdessen werden sie lediglich darüber informiert, dass es ihnen nicht gelungen ist, den Wert zu aktualisieren. Die Threads können dann weiterarbeiten und Kontextwechsel werden vollständig vermieden.

Eine andere Konsequenz ist, dass die Kernprogrammlogik komplexer wird. Dies liegt daran, dass wir das Szenario behandeln müssen, in dem die CAS-Operation nicht erfolgreich war. Wir können es immer wieder versuchen, bis es erfolgreich ist, oder wir können nichts tun und je nach Anwendungsfall weitermachen.

4. Atomvariablen in Java

Die in Java am häufigsten verwendeten atomaren Variablenklassen sindAtomicInteger,AtomicLong,AtomicBoolean undAtomicReference. Diese Klassen repräsentierenint,long,boolean und Objektreferenz, die atomar aktualisiert werden können. Die wichtigsten Methoden dieser Klassen sind:

  • get() - Ruft den Wert aus dem Speicher ab, sodass Änderungen, die von anderen Threads vorgenommen wurden, sichtbar sind. entspricht dem Lesen einervolatile-Variablen

  • set() - schreibt den Wert in den Speicher, sodass die Änderung für andere Threads sichtbar ist; entspricht dem Schreiben einervolatile-Variablen

  • lazySet() - schreibt schließlich den Wert in den Speicher und kann mit nachfolgenden relevanten Speicheroperationen neu angeordnet werden. Ein Anwendungsfall ist das Aufheben von Verweisen zum Zwecke der Speicherbereinigung, auf die nie wieder zugegriffen wird. In diesem Fall wird eine bessere Leistung erzielt, indem das Schreiben von nullvolatileverzögert wird

  • compareAndSet() - wie in Abschnitt 3 beschrieben, gibt true zurück, wenn dies erfolgreich ist, andernfalls false

  • weakCompareAndSet() - wie in Abschnitt 3 beschrieben, jedoch schwächer in dem Sinne, dass keine Vorbestellungen erstellt werden. Dies bedeutet, dass möglicherweise nicht unbedingt Aktualisierungen anderer Variablen angezeigt werden

Ein mitAtomicInteger implementierter thread-sicherer Zähler ist im folgenden Beispiel dargestellt:

public class SafeCounterWithoutLock {
    private final AtomicInteger counter = new AtomicInteger(0);

    public int getValue() {
        return counter.get();
    }
    public void increment() {
        while(true) {
            int existingValue = getValue();
            int newValue = existingValue + 1;
            if(counter.compareAndSet(existingValue, newValue)) {
                return;
            }
        }
    }
}

Wie Sie sehen können, wiederholen wir die OperationcompareAndSetund erneut bei einem Fehler, da wir sicherstellen möchten, dass der Aufruf der Methodeincrementden Wert immer um 1 erhöht.

5. Fazit

In diesem kurzen Tutorial haben wir eine alternative Methode für den Umgang mit Parallelität beschrieben, bei der die mit dem Sperren verbundenen Nachteile vermieden werden können. Wir haben uns auch die wichtigsten Methoden angesehen, die von den Klassen atomarer Variablen in Java bereitgestellt werden.

Wie immer sind alle Beispieleover on GitHub verfügbar.

Weitere Klassen, die intern nicht blockierende Algorithmen verwenden, finden Sie untera guide to ConcurrentMap.