Voraussetzungen für die Entstehung atomarer Operationen

Schauen wir uns dieses Beispiel an, um zu verstehen, wie atomare Operationen funktionieren:

public class Counter {
    int count;

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

Wenn wir einen Thread haben, funktioniert alles großartig, aber wenn wir Multithreading hinzufügen, erhalten wir falsche Ergebnisse, und das alles, weil die Inkrementierungsoperation nicht eine Operation ist, sondern drei: eine Anfrage, um den aktuellen Wert zu erhaltenzählen, dann um 1 erhöhen und erneut schreibenzählen.

Und wenn zwei Threads eine Variable erhöhen möchten, gehen höchstwahrscheinlich Daten verloren. Das heißt, beide Threads erhalten 100, beide schreiben also 101 statt des erwarteten Wertes 102.

Und wie kann man es lösen? Sie müssen Schlösser verwenden. Das Schlüsselwort „synced“ hilft bei der Lösung dieses Problems, da es Ihnen die Garantie gibt, dass immer nur ein Thread auf die Methode zugreift.

public class SynchronizedCounterWithLock {
    private volatile int count;

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

Außerdem müssen Sie das Schlüsselwort volatile hinzufügen , das die korrekte Sichtbarkeit von Referenzen zwischen Threads gewährleistet. Wir haben seine Arbeit oben besprochen.

Aber es gibt immer noch Nachteile. Der größte Nachteil ist die Leistung. Zu dem Zeitpunkt, an dem viele Threads versuchen, eine Sperre zu erhalten, und einer eine Schreibmöglichkeit erhält, werden die restlichen Threads entweder blockiert oder angehalten, bis der Thread freigegeben wird.

Alle diese Prozesse, Blockierungen und das Umschalten in einen anderen Status, beeinträchtigen die Systemleistung sehr.

Atomare Operationen

Der Algorithmus verwendet Low-Level-Maschinenanweisungen wie Compare-and-Swap (CAS, Compare-and-Swap, das die Datenintegrität gewährleistet und zu dem bereits umfangreiche Untersuchungen durchgeführt wurden).

Eine typische CAS-Operation arbeitet mit drei Operanden:

  • Speicherplatz für Arbeit (M)
  • Vorhandener Erwartungswert (A) einer Variablen
  • Neuer Wert (B) soll eingestellt werden

CAS aktualisiert M atomar auf B, aber nur, wenn der Wert von M mit A übereinstimmt, andernfalls wird keine Aktion ausgeführt.

Im ersten und zweiten Fall wird der Wert von M zurückgegeben. Dadurch können Sie drei Schritte kombinieren, nämlich das Abrufen des Werts, das Vergleichen des Werts und das Aktualisieren. Und auf Maschinenebene wird alles zu einem einzigen Arbeitsgang.

Sobald eine Multithread-Anwendung auf eine Variable zugreift und versucht, sie zu aktualisieren, und CAS angewendet wird, erhält einer der Threads die Variable und kann sie aktualisieren. Im Gegensatz zu Sperren erhalten andere Threads jedoch lediglich die Fehlermeldung, dass der Wert nicht aktualisiert werden kann. Dann geht es weiter mit der Arbeit, ein Wechsel ist bei dieser Art von Arbeit völlig ausgeschlossen.

In diesem Fall wird die Logik schwieriger, da wir mit der Situation umgehen müssen, in der die CAS-Operation nicht erfolgreich funktioniert hat. Wir modellieren den Code einfach so, dass er nicht weitergeht, bis der Vorgang erfolgreich ist.

Einführung in Atomtypen

Sind Sie auf eine Situation gestoßen, in der Sie die Synchronisierung für die einfachste Variable vom Typ int einrichten müssen ?

Die erste Möglichkeit, die wir bereits behandelt haben, ist die Verwendung von volatile + synchronisiert . Es gibt aber auch spezielle Atomic*-Klassen.

Wenn wir CAS verwenden, funktionieren die Operationen im Vergleich zur ersten Methode schneller. Darüber hinaus verfügen wir über spezielle und sehr praktische Methoden zum Addieren eines Werts sowie für Inkrementierungs- und Dekrementierungsoperationen.

AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray sind Klassen, in denen Operationen atomar sind. Im Folgenden analysieren wir die Arbeit mit ihnen.

AtomicInteger

Die AtomicInteger- Klasse stellt Operationen für einen int- Wert bereit , der atomar gelesen und geschrieben werden kann, zusätzlich zu erweiterten atomaren Operationen.

Es verfügt über Get- und Set- Methoden , die wie das Lesen und Schreiben von Variablen funktionieren.

Das heißt, „passiert vorher“ bei jedem späteren Empfang derselben Variablen, über die wir zuvor gesprochen haben. Die atomare Methode „compareAndSet“ verfügt ebenfalls über diese Speicherkonsistenzfunktionen.

Alle Operationen, die einen neuen Wert zurückgeben, werden atomar ausgeführt:

int addAndGet (int delta) Fügt dem aktuellen Wert einen bestimmten Wert hinzu.
boolean CompareAndSet(expected int, update int) Setzt den Wert auf den angegebenen aktualisierten Wert, wenn der aktuelle Wert mit dem erwarteten Wert übereinstimmt.
int decrementAndGet() Verringert den aktuellen Wert um eins.
int getAndAdd(int delta) Addiert den angegebenen Wert zum aktuellen Wert.
int getAndDecrement() Verringert den aktuellen Wert um eins.
int getAndIncrement() Erhöht den aktuellen Wert um eins.
int getAndSet(int newValue) Setzt den angegebenen Wert und gibt den alten Wert zurück.
int inkrementAndGet() Erhöht den aktuellen Wert um eins.
lazySet(int newValue) Zum Schluss auf den angegebenen Wert einstellen.
boolescher SchwachwertCompareAndSet(erwartet, int aktualisieren) Setzt den Wert auf den angegebenen aktualisierten Wert, wenn der aktuelle Wert mit dem erwarteten Wert übereinstimmt.

Beispiel:

ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);

System.out.println(atomicInteger.get()); // prints 50