W3docs

Java Atomare Variablen

Lock-freie threadsichere Operationen in Java mit java.util.concurrent.atomic — Zähler, Referenzen und compare-and-set.

volatile macht einzelne Lese- oder Schreiboperationen threadsicher. Es kann counter++ nicht threadsicher machen — das sind drei Operationen. Das Paket java.util.concurrent.atomic schließt diese Lücke. Seine Klassen kapseln einen einzelnen Wert und stellen Operationen wie increment-and-get und compare-and-set als einzelne atomare Instruktionen bereit — kein Lock, kein synchronized-Block, nur ein CPU-Primitiv (compare-and-swap, kurz CAS), das die JVM bis auf Maschinenebene kompiliert.

Atomics sind das richtige Werkzeug für eine überraschend große Anzahl von Multithreading-Mustern: Zähler, Sequenznummern, flag-artiger Zustand und jedes „einen neuen unveränderlichen Snapshot veröffentlichen"-Idiom. Sie sind unter Contention schneller als synchronized und deutlich einfacher als ein manueller Lock um ein einzelnes Feld.

Die Familie

Das Paket enthält acht häufig verwendete Klassen:

KlasseKapseltGängige Operationen
AtomicIntegerintget, set, incrementAndGet, addAndGet, compareAndSet
AtomicLonglongdieselben wie oben, auf long
AtomicBooleanbooleanget, set, compareAndSet
AtomicReference<V>V (beliebige Objektreferenz)get, set, compareAndSet, updateAndGet
AtomicIntegerArrayint[]atomare Ops pro Index
AtomicLongArraylong[]atomare Ops pro Index
AtomicReferenceArray<V>V[]atomare Ops pro Index
LongAdder / LongAccumulatorlongZähler bei hoher Contention

Die ersten vier werden in 99 % der Fälle benötigt.

AtomicInteger — der richtige Zähler

Der Ersatz für „volatile int plus ++":

AtomicInteger counter = new AtomicInteger();          // starts at 0

counter.incrementAndGet();                            // ++counter, atomic
counter.getAndIncrement();                            // counter++, atomic
counter.addAndGet(5);                                 // counter += 5, atomic
counter.set(42);                                      // counter = 42, atomic
int n = counter.get();                                // read

counter.compareAndSet(42, 100);                       // if (counter == 42) counter = 100; return whether it changed

incrementAndGet ist der richtige Ansatz für einen einfachen Zähler; intern ist es eine CAS-Schleife, die die CPU in einer einzigen Instruktion auf modernem x86 ausführt (LOCK XADD). Die gesamte Operation ist eine einzelne Speichertransaktion auf Busebene — deutlich günstiger als das Erwerben eines unstrittigen synchronized-Locks.

compareAndSet(expected, new) ist der Baustein für nahezu alles andere. Es schreibt new atomar nur dann, wenn der aktuelle Wert expected ist, und gibt zurück, ob der Schreibvorgang stattgefunden hat. Damit lässt sich jede atomare Aktualisierung eines einzelnen Felds realisieren:

AtomicInteger max = new AtomicInteger(Integer.MIN_VALUE);
void recordMax(int v) {
  int cur;
  do {
    cur = max.get();
    if (v <= cur) return;                             // nothing to do
  } while (!max.compareAndSet(cur, v));               // retry if someone else updated
}

Die CAS-Schleife ist das Standardmuster: lesen, berechnen, versuchen zu schreiben, bei Konflikt wiederholen. So ist incrementAndGet implementiert; so schreibt man jede zusammengesetzte Aktualisierung eines einzelnen Felds.

Java 8 vereinfachte die Schleife:

max.updateAndGet(cur -> Math.max(cur, v));            // CAS loop hidden

updateAndGet, accumulateAndGet und getAndUpdate nehmen eine Funktion und führen die CAS-Schleife für einen durch. Verwende sie, wenn sie passen.

AtomicReference<V> — der richtige Weg, ein Objekt zu tauschen

Wenn der gemeinsame Zustand mehr als ein Primitiv ist — eine Konfigurations-Map, ein gecachter Snapshot, ein unveränderlicher Holder — ermöglicht AtomicReference das atomare Austauschen des gesamten Objekts:

AtomicReference<Config> currentConfig = new AtomicReference<>(initialConfig);

void reload() {
  Config c = readConfigFromDisk();                    // expensive, lock-free
  currentConfig.set(c);                               // publish atomically
}

Config get() { return currentConfig.get(); }

Der Trick: Der Inhalt von Config muss unveränderlich sein (oder nach dem Veröffentlichen nicht mehr angefasst werden). Der atomare Swap veröffentlicht einen fertigen Wert; wenn andere Threads danach die Interna des Werts mutieren, ist die Sicherheit verloren. Dies ist das unveränderliche Snapshot-Muster, auf dem die meisten nebenläufigen Caches, Routing-Tabellen und „globale Konfigurations"-Objekte basieren.

updateAndGet auf einer Referenz ist ebenfalls äußerst nützlich:

AtomicReference<List<String>> log = new AtomicReference<>(List.of());

void append(String line) {
  log.updateAndGet(old -> {
    var copy = new ArrayList<>(old);
    copy.add(line);
    return List.copyOf(copy);                         // immutable snapshot
  });
}

Jeder Leser erhält eine konsistente unveränderliche Liste. Schreiber wetteifern; die CAS-Schleife wiederholt die wenigen, die das Rennen verlieren. Günstig bei geringer Contention, langsam aber korrekt bei hoher.

LongAdder — der Zähler bei hoher Contention

Unter starker Contention wird AtomicLong.incrementAndGet zum Engpass — jeder Thread hämmert auf dieselbe Speicheradresse, und die CPU muss die Bus-Transaktionen serialisieren. LongAdder löst dies, indem es mehrere interne Zähler verwaltet, einen pro CPU, und sie beim Lesen summiert:

LongAdder requestCount = new LongAdder();

void onRequest() { requestCount.increment(); }        // append-only, no contention

long snapshot() { return requestCount.sum(); }        // sums every cell — not atomic but eventually consistent

Verwende LongAdder, wenn:

  • Der Zähler von vielen Threads gleichzeitig inkrementiert wird (z. B. Anfragemetriken in einem Webserver).
  • Du ihn selten liest (alle paar Sekunden für ein Dashboard).

Verwende AtomicLong, wenn:

  • Das Inkrementieren selten oder single-threaded ist.
  • Du einen sofortigen, genauen Lesewert benötigst.

LongAdder ist einer der schnellsten nebenläufigen Zähler überhaupt — der Kompromiss ist, dass sum() nicht atomar mit nebenläufigen Inkrementen ist. Für den typischen Metrik-Reporting-Anwendungsfall ist das in Ordnung.

Was Atomics nicht sind

Atomics skalieren auf ein einzelnes Feld. Sie lassen sich nicht über mehrere Felder hinweg zusammensetzen:

AtomicInteger a = new AtomicInteger();
AtomicInteger b = new AtomicInteger();

a.incrementAndGet();                                  // atomic on its own
b.incrementAndGet();                                  // atomic on its own
// but the pair is NOT atomic — another thread can see new a, old b

Wenn eine Invariante mehrere Felder umspannt ("a == b + 1 immer"), wird ein Lock benötigt (oder ein einzelnes Atomic auf einem Holder-Objekt, das beide enthält).

Atomics helfen auch nicht bei der Sichtbarkeit nicht verwandter Felder. Das Schreiben in ein Atomic veröffentlicht andere Felder nicht so wie volatile. Mache jene anderen Felder volatile (oder final, oder schreibe sie durch das Atomic).

compareAndExchange und die neue API (Java 9+)

Java 9 fügte compareAndExchange hinzu (gibt den aktuellen Wert zurück, nicht nur ein boolean):

int prev = counter.compareAndExchange(expected, newVal);
if (prev == expected) {                               // we won
  ...
} else {                                              // somebody else got there first
  // prev is the actual current value
}

Java 9 fügte auch die VarHandle-API hinzu, die schwaches CAS, geordneten Zugriff usw. für Low-Level-Nebenläufigkeitsbibliotheken bereitstellt. Man braucht sie sehr selten; hier wird der Name erwähnt, damit er bekannt ist.

Ein ausgearbeitetes Beispiel: Zähler und Snapshot

Das folgende Programm vergleicht vier Zähler: unsynchronisiert, volatile, AtomicInteger und LongAdder. Alle vier werden von 8 Threads mit jeweils 100.000 Inkrementen belastet.

java— editable, runs on the server

Was aus dem Lauf zu mitnehmen ist:

  • plain und volatile haben beide Aktualisierungen verloren — manchmal dramatisch (ein Endwert weit unter dem erwarteten 800.000). volatile behebt das Sichtbarkeitsproblem, aber n++ sind immer noch drei Operationen. Das ist das Wichtigste, das man über volatile wissen muss: es macht zusammengesetzte Aktualisierungen nicht atomar.
  • AtomicInteger lieferte bei jedem Lauf exakt den erwarteten Zählerstand. Die Kosten pro Inkrement betrugen wenige Nanosekunden — deutlich höher als n++ auf einem einfachen int (ein oder zwei), aber keine Lock-Akquisitionen und kein Thread-Blocking. Unter Contention ist es deutlich schneller als synchronized.
  • LongAdder war der schnellste Zähler unter der 8-Thread-Last — er streut Schreibvorgänge auf separate Zellen pro CPU, sodass die Threads nicht auf einer einzigen Cache-Line konkurrieren. Der Kompromiss ist, dass sum() nicht atomar mit increment() ist (ein Leser kann einen leicht veralteten Gesamtwert sehen), was für Metriken und Zähler, bei denen keine sofortige Genauigkeit erforderlich ist, genau das Richtige ist.
  • Die CAS-Schleife für den maximalen Wert hat den größten gesehenen Wert über alle Stichproben hinweg aufgezeichnet. Die Schleife ist das allgemeine Muster: den aktuellen Wert lesen, den gewünschten neuen Wert berechnen, versuchen zu schreiben; wenn jemand anderes zuerst geschrieben hat, schlägt der CAS fehl und man wiederholt. Die meisten updateAndGet- und accumulateAndGet-Aufrufe sind diese Schleife mit verstecktem Boilerplate.
  • AtomicReference<List<String>> erzeugte einen unveränderlichen Snapshot des Logs. Jeder Schreiber baute eine neue unveränderliche Kopie und versuchte sie zu veröffentlichen; unter Contention könnten zwei Schreiber beide eine Kopie erstellen, und der CAS eines schlägt fehl — dieser Thread wiederholt, liest die frisch aktualisierte Liste und führt zusammen. Das Muster ist unter starker Contention verschwenderisch (viele weggeworfene Kopien), aber ideal für „leselastige, gelegentlich neu erstellte" Snapshots.

Was kommt als Nächstes

Das nächste Kapitel, Java Locks, beginnt die Geschichte von java.util.concurrent.locks — das Lock-Interface, warum es neben synchronized existiert, und die Fähigkeiten (tryLock, lockInterruptibly, Condition), die es hinzufügt und die der intrinsische Monitor nicht hat.

Übungen

Übung
Welche dieser Optionen ist der korrekte Weg, einen gemeinsamen Zähler von vielen Threads in einer engen Schleife sicher zu inkrementieren?
Welche dieser Optionen ist der korrekte Weg, einen gemeinsamen Zähler von vielen Threads in einer engen Schleife sicher zu inkrementieren?
Was this page helpful?