1. Warum funktioniert i++ in der Nebenläufigkeit nicht?
Beginnen wir mit einer klassischen Aufgabe: Wir haben eine Zählervariable, zum Beispiel die Anzahl verarbeiteter Anfragen oder heruntergeladener Dateien. Mehrere Threads sollen diesen Zähler erhöhen. Was kann schiefgehen, wenn man einfach i++ schreibt?
Beispiel: Datenrennen beim Inkrement
public class Counter {
public int count = 0;
public void increment() {
count++; // Nicht atomar!
}
}
Stellen wir uns vor, zwei Threads rufen gleichzeitig increment() auf. Beide Threads lesen den alten Wert, beide erhöhen ihn um 1, und beide schreiben … denselben neuen Wert zurück! Dadurch geht ein Inkrement „verloren“. Wenn man das oft wiederholt, ist der Endwert kleiner als erwartet.
Warum passiert das?
Die Operation i++ besteht in Wirklichkeit aus drei Schritten:
- Den Wert der Variablen lesen (zum Beispiel 5).
- Diesen Wert um 1 erhöhen.
- Den neuen Wert zurück in den Speicher schreiben.
In einer Multithread-Umgebung kann ein anderer Thread die Variable zwischen diesen Schritten ändern. Ergebnis — ein „Datenrennen“ (Race Condition).
Was sind atomare Operationen?
Eine atomare Operation ist eine Aktion, die entweder vollständig ausgeführt wird oder gar nicht, und kein anderer Thread kann „mittendrin“ eingreifen.
In Java gibt es eine Reihe von Klassen, die solche Operationen für Primitive und Referenzen bereitstellen. Sie befinden sich im Paket java.util.concurrent.atomic. Die beliebtesten sind:
- AtomicInteger — atomarer Ganzzahltyp.
- AtomicLong — atomarer long.
- AtomicBoolean — atomarer boolean.
- AtomicReference<T> — atomare Referenz auf ein Objekt beliebigen Typs.
2. AtomicInteger: threadsicherer Zähler
Deklaration und grundlegende Verwendung
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Atomare Erhöhung
}
public int get() {
return count.get();
}
}
Hier führt incrementAndGet() „erhöhen und den neuen Wert zurückgeben“ als eine unteilbare Operation aus. Selbst wenn 100 Threads diese Methode gleichzeitig aufrufen, geht kein Inkrement verloren.
Nützliche Methoden:
| Methode | Beschreibung |
|---|---|
|
Aktuellen Wert abrufen |
|
Wert setzen |
|
Um 1 erhöhen und neuen Wert zurückgeben |
|
Aktuellen Wert zurückgeben und um 1 erhöhen |
|
Um delta erhöhen und neuen Wert zurückgeben |
|
Wenn der aktuelle Wert expect entspricht, auf update setzen (CAS) |
Beispiel: Multithread-Zähler
Nehmen wir an, es gibt eine Klasse, die die Anzahl verarbeiteter Nachrichten in einem Chat zählt.
public class MessageStatistics {
private final AtomicInteger messageCount = new AtomicInteger(0);
public void onMessageReceived() {
int newCount = messageCount.incrementAndGet();
System.out.println("Gesamtzahl der Nachrichten: " + newCount);
}
public int getMessageCount() {
return messageCount.get();
}
}
Unter der Haube: Wie funktioniert AtomicInteger?
Intern verwendet AtomicInteger eine spezielle Prozessorinstruktion — CAS (Compare-And-Swap, „vergleichen‑und‑austauschen“). Das ist eine atomare Operation, die den aktuellen Wert einer Variablen mit dem erwarteten vergleicht und, wenn sie übereinstimmen, den neuen Wert schreibt. Wenn ein anderer Thread die Variable inzwischen geändert hat, wird die Operation nicht ausgeführt und der Versuch wiederholt.
Ablauf:
1. Aktuellen Wert lesen (z. B. 5)
2. Mit erwartetem Wert vergleichen (5)
3. Wenn gleich — neuen Wert schreiben (6)
4. Wenn nicht gleich — erneut versuchen
All das geschieht sehr schnell und ohne Sperren (lock‑free). Daher sind atomare Klassen oft schneller als synchronized, besonders bei vielen Threads.
3. AtomicReference: atomare Referenz auf ein Objekt
AtomicReference<T> ist ein universeller atomarer Container für beliebige Objekte. Er ermöglicht es, die Referenz auf ein Objekt aus verschiedenen Threads sicher zu ändern.
Beispiel: Threadsicheres Aktualisieren einer Referenz
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceExample {
private final AtomicReference<String> latestMessage = new AtomicReference<>("");
public void updateMessage(String message) {
latestMessage.set(message);
}
public String getLatestMessage() {
return latestMessage.get();
}
}
Einsatz von compareAndSet
Die interessanteste Operation ist compareAndSet(expected, newValue). Sie aktualisiert den Wert nur, wenn er sich seit dem letzten Lesen nicht geändert hat.
public void safeUpdate(String oldValue, String newValue) {
boolean success = latestMessage.compareAndSet(oldValue, newValue);
if (success) {
System.out.println("Aktualisierung war erfolgreich!");
} else {
System.out.println("Jemand hat den Wert bereits geändert, bitte versuchen Sie es erneut.");
}
}
Das ist die Grundlage nicht blockierender Algorithmen: von Warteschlangen und Stacks bis hin zu Caches, wo es wichtig ist, unnötige Sperren zu vermeiden.
4. Beispiele für den Einsatz in Anwendungen
Beispiel 1: Multithread-Nachrichtenzähler
public class ChatRoom {
private final AtomicInteger messageCount = new AtomicInteger(0);
public void receiveMessage(String message) {
// ... Nachrichtenverarbeitung ...
int count = messageCount.incrementAndGet();
System.out.println("Neue Nachricht: " + message + ". Gesamtzahl der Nachrichten: " + count);
}
}
Beispiel 2: Sicheres Aktualisieren der Referenz auf die letzte Nachricht
public class ChatRoom {
private final AtomicReference<String> lastMessage = new AtomicReference<>("");
public void receiveMessage(String message) {
lastMessage.set(message);
// ... Verarbeitung ...
}
public String getLastMessage() {
return lastMessage.get();
}
}
Wenn die Referenz nur aktualisiert werden soll, sofern sich die letzte Nachricht nicht geändert hat (um einen „Verlust“ bei gleichzeitigen Aktualisierungen zu vermeiden), verwenden Sie compareAndSet.
5. Einschränkungen und Fallstricke
Wann sind atomare Klassen kein Allheilmittel?
Atomare Variablen eignen sich hervorragend für einfache Operationen: Inkrement, Wert setzen, prüfen und ersetzen. Müssen jedoch mehrere Variablen gleichzeitig aktualisiert werden, ist die Atomarität nicht mehr gewährleistet. Wenn Sie beispielsweise zwei Zähler haben und beide als eine Operation erhöhen möchten — hier braucht man synchronized oder einen anderen Synchronisationsmechanismus.
Beispiel für eine falsche Verwendung
// Nicht atomar!
if (ref.get() == null) {
ref.set("Hello");
}
Zwischen get() und set(...) kann ein anderer Thread den Wert ändern, und die Bedingung ist dann nicht mehr gültig. Für solche Fälle verwenden Sie compareAndSet.
Atomare Klassen ≠ threadsichere Objekte
Wenn das Objekt, auf das AtomicReference zeigt, selbst nicht threadsicher ist, dann ist zwar der Austausch der Referenz atomar, nicht jedoch das Ändern der Felder des Objekts. Wenn Sie zum Beispiel in AtomicReference<List<String>> eine normale ArrayList speichern, wird die Liste dadurch nicht threadsicher.
6. Fortgeschrittene atomare Klassen
Im Paket java.util.concurrent.atomic gibt es weitere nützliche Klassen:
- AtomicLong, AtomicBoolean — für long und boolean.
- AtomicIntegerArray, AtomicReferenceArray — atomare Operationen für Arrays.
- LongAdder, LongAccumulator — für stark belastete Zähler.
LongAdder und LongAccumulator
Wenn Sie sehr viele Threads haben und ein gewöhnlicher AtomicInteger zum „Flaschenhals“ wird (alle Threads konkurrieren um eine Variable), verwenden Sie LongAdder. Er teilt den Zähler in mehrere interne Zellen auf und summiert sie bei der Abfrage des Werts, was bei hoher Konkurrenz Vorteile bringt.
import java.util.concurrent.atomic.LongAdder;
public class FastCounter {
private final LongAdder adder = new LongAdder();
public void increment() {
adder.increment();
}
public long getCount() {
return adder.sum();
}
}
7. Typische Fehler im Umgang mit atomaren Variablen
Fehler Nr. 1: Atomarität bei komplexen Operationen erwarten.
Wenn mehrere Schritte auf dem Wert ausgeführt werden müssen, helfen atomare Klassen nicht — zwischen den Schritten kann ein anderer Thread die Daten ändern. Für zusammengesetzte Operationen verwenden Sie compareAndSet oder Synchronisation.
Fehler Nr. 2: Threadsicherheit verschachtelter Objekte ignorieren.
Liegt in AtomicReference ein normales Objekt, werden dessen Methoden und Felder dadurch nicht threadsicher. Atomar ist nur der Austausch der Referenz.
Fehler Nr. 3: Atomare Klassen ohne Notwendigkeit einsetzen.
In Single‑Thread‑Code sind atomare Typen überflüssig und aufgrund zusätzlicher Prüfungen etwas langsamer als normale Variablen.
Fehler Nr. 4: Vorzeitige Optimierung.
Manchmal ist es einfacher und zuverlässiger, synchronized zu verwenden, insbesondere wenn die Logik komplex ist und mehrere Variablen gleichzeitig betrifft. Lock‑free‑Lösungen sind nicht immer gerechtfertigt.
Fehler Nr. 5: Das ABA‑Problem vergessen.
Ein seltener, aber wichtiger Fall: Der Wert ändert sich von A nach B und wieder zurück nach A — compareAndSet „denkt“, es habe sich nichts geändert. Für solche Szenarien verwenden Sie Spezialklassen wie AtomicStampedReference (oder AtomicMarkableReference).
GO TO FULL VERSION