1. Vergessenes unlock/release: Falle für Unaufmerksame
Einer der tückischsten Fehler beim Einsatz moderner Synchronisationswerkzeuge wie ReentrantLock oder Semaphore ist es, unlock() oder release() zu vergessen. Wenn Sie die Sperre nicht freigeben, warten andere Threads auf deren Freigabe … ewig. Das Programm hängt, und Sie starren lange auf den Bildschirm und fragen sich, warum nichts passiert.
Betrachten wir ein Beispiel mit ReentrantLock:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
// Ups! unlock() vergessen — jetzt hängen alle fest!
count++;
}
}
Das sieht harmlos aus, aber wenn Sie increment() mehrfach aus verschiedenen Threads aufrufen, werden nach dem ersten Aufruf die übrigen Threads auf die Freigabe der Sperre unendlich warten.
Um dies zu vermeiden, verwenden Sie ein try-finally-Konstrukt:
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
Jetzt wird die Sperre selbst dann garantiert freigegeben, wenn in der Mitte der Methode eine Ausnahme auftritt.
Das ist so, als würde jemand die Toilette besetzen (von innen abschließen) und dann vergessen, die Tür zu öffnen, und durch das Fenster hinausgehen. Die anderen warten, bis diese Person herauskommt … Tun Sie das nicht!
2. Synchronisieren am falschen Objekt: „Ups, das Schloss an der falschen Stelle!“
In Java kann das Schlüsselwort synchronized den Zugriff auf ein Objekt sperren. Wählen Sie jedoch das falsche Objekt für die Sperre, funktioniert die Synchronisation nicht wie erwartet.
Fehler Nr. 1: Synchronisation auf einer lokalen Variablen
public void doSomething() {
Object lock = new Object();
synchronized (lock) {
// Jedes Mal ein neues Objekt — keine Synchronisation!
// Threads warten nicht aufeinander.
// Der kritische Abschnitt ist ungeschützt!
}
}
Hier erstellt jeder Thread sein eigenes Objekt lock. Dadurch findet keine echte Sperre statt – die Threads betreten den kritischen Abschnitt gleichzeitig.
Richtig:
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// Jetzt verwenden alle Threads dasselbe lock-Objekt
// und warten tatsächlich aufeinander.
}
}
Fehler Nr. 2: Synchronisation auf einem String-Literal
public void doSomething() {
synchronized ("lock") {
// String-Literale sind interned: unterschiedliche Programmteile können
// sich versehentlich auf demselben String synchronisieren!
}
}
Fazit:
Synchronisieren Sie nur auf privaten, eigens dafür angelegten Objekten, die nirgendwo sonst verwendet werden.
3. Wechselseitige Blockierung (Deadlock): „Du mir – ich dir, und beide stehen still“
Deadlock (gegenseitige Blockierung) ist ein Klassiker. Zwei (oder mehr) Threads erwerben abwechselnd unterschiedliche Sperren und warten aufeinander, bis das Programm vollständig zum Stillstand kommt.
Beispiel:
public class DeadlockExample {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized (lockA) {
// Kurz warten für einen reproduzierbaren Effekt
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (lockB) {
// ...
}
}
}
public void method2() {
synchronized (lockB) {
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (lockA) {
// ...
}
}
}
}
Wenn ein Thread method1() und ein anderer – method2() aufruft, erwirbt der erste Thread lockA und wartet auf lockB, und der zweite – umgekehrt. Am Ende warten beide bis in alle Ewigkeit aufeinander.
Wie vermeiden?
- Erwerben Sie Sperren in allen Threads stets in derselben Reihenfolge.
- Minimieren Sie die Anzahl gleichzeitig gehaltener Sperren.
- Nutzen Sie Diagnosewerkzeuge (z. B. jstack), wenn das Programm hängt.
Analogie:
Das ist, als würden sich zwei Personen in einem engen Flur begegnen, und jeder will nur dann den Weg freimachen, wenn der andere zuerst Platz macht. Am Ende bleiben beide stehen und warten, bis jemand als Erster nachgibt.
4. Übermäßige Synchronisation: „Lieber zu viel als zu wenig?“ – nicht immer!
Manchmal synchronisieren Entwickler aus Angst vor Fehlern alles Mögliche. Das Ergebnis: Die Leistung sinkt, der Nutzen ist gleich null.
Beispiel:
public synchronized void add(int value) {
// Hier gibt es nur eine Zeile, die keine Synchronisation benötigt!
System.out.println("Hinzugefügt: " + value);
}
In diesem Fall ist Synchronisation nicht nötig: Die Ausgabe über System.out.println ist bereits threadsicher, und die Methode arbeitet nicht mit gemeinsamen Ressourcen.
Wo ist das kritisch?
Wenn Sie Methoden synchronisieren, die häufig aufgerufen werden und keinen Schutz benötigen, verringern Sie die Programmbarkeit drastisch. Threads stellen sich in eine Warteschlange, obwohl sie parallel arbeiten könnten.
Best Practice:
Synchronisieren Sie nur das, was wirklich nötig ist. Der kritische Abschnitt sollte so klein wie möglich sein.
5. Falsche Verwendung von volatile: „Sichtbarkeit ja, Atomarität nein!“
Der Modifikator volatile in Java garantiert, dass Änderungen an einer Variablen für alle Threads sichtbar sind. Aber er garantiert nicht die Atomarität von Operationen.
Fehler:
private volatile int counter = 0;
public void increment() {
counter++; // Nicht atomar!
}
Die Operation counter++ besteht aus Lesen des Werts, Erhöhen und Zurückschreiben. Führen zwei Threads diesen Code gleichzeitig aus, kann der Endwert kleiner als erwartet sein.
Richtig:
Für atomare Operationen verwenden Sie synchronized, AtomicInteger oder andere threadsichere Klassen.
import java.util.concurrent.atomic.AtomicInteger;
private final AtomicInteger counter = new AtomicInteger();
public void increment() {
counter.incrementAndGet();
}
Wann sollte man volatile verwenden?
Für einfache Flags (zum Beispiel „Beenden“), wenn keine Atomarität erforderlich ist.
GO TO FULL VERSION