BY Abitur Informatik gA study materials
BY Abitur Informatik gA study resources

Verlorene Einzahlungen und Deadlock-Gefahr bei Überweisungen

BY Abitur Informatik gA · Put your knowledge to work in context.

All case studies

Verlorene Einzahlungen und Deadlock-Gefahr bei Überweisungen

Eine Bank simuliert Überweisungen in Java. Die Klasse Konto hat ein Feld stand (int, in Cent) und die Methoden einzahlen(int betrag) und auszahlen(int betrag). In einem Testlauf starten zwei Threads gleichzeitig und rufen je 1000-mal einzahlen(100) auf demselben Konto auf, Startstand 0. Erwartet werden 200 000 Cent; beobachtet werden je nach Lauf Werte zwischen 187 300 und 199 900. In einem zweiten Test prüft ein Thread mit if (stand >= betrag) stand = stand - betrag;, ob gedeckt ist; gelegentlich entsteht ein negativer Stand. Ein Praktikant will das Problem mit dem Schlüsselwort volatile lösen. Für Überweisungen zwischen zwei Konten sollen später beide Konten gesperrt werden; ein Kollege warnt vor Deadlocks. Die Fachlehrkraft verlangt eine Analyse mit Begründung, keine reine Symptombehebung.

1. Erklären Sie am Beispiel von einzahlen(100), wie die verlorenen Aktualisierungen entstehen, und begründen Sie, warum volatile das Problem nicht löst.

Read the worked answer

Read-Modify-Write. Die Anweisung stand = stand + 100 ist keine unteilbare Operation, sondern mindestens drei Schritte: (1) stand lesen, (2) 100 addieren, (3) Ergebnis zurückschreiben. Ohne Synchronisation kann folgende Verschachtelung auftreten:

Thread A: liest stand = 500
Thread B: liest stand = 500
Thread A: schreibt 600
Thread B: schreibt 600

Eine der beiden Erhöhungen ist verloren; der Zähler steht bei 600 statt 700. Bei 2000 Einzahlungen und häufigem Kontextwechsel summieren sich solche Verluste – 187 300 entspricht 127 verlorenen Inkrementen, was bei 2000 Operationen und zwei Threads plausibel ist.

Warum volatile nicht hilft. volatile garantiert Sichtbarkeit: Jeder Thread liest den zuletzt geschriebenen Wert, nicht einen veralteten Cache. Es garantiert aber keine Atomarität der Sequenz aus Lesen, Addieren und Schreiben. Zwei Threads können weiterhin denselben Wert lesen; volatile verhindert nur, dass ein Thread einen längst überschriebenen Wert sieht. Das Problem ist ein Wettlauf zwischen Operationen, nicht ein Speichersichtbarkeitsproblem. Auch die Prüfung if (stand >= betrag) ist ein check-then-act: Zwischen Prüfung und Subtraktion kann ein anderer Thread den Stand verändern, daher der negative Stand.

2. Zeigen Sie eine korrigierte Java-Implementierung mit synchronized und begründen Sie, worauf beim Sperren zu achten ist.

Read the worked answer

Korrektur mit Methodensynchronisation:

public class Konto {
    private int stand;

    public synchronized void einzahlen(int betrag) {
        if (betrag <= 0) throw new IllegalArgumentException();
        stand += betrag;
    }

    public synchronized boolean auszahlen(int betrag) {
        if (betrag <= 0) throw new IllegalArgumentException();
        if (stand < betrag) return false;
        stand -= betrag;
        return true;
    }

    public synchronized int getStand() { return stand; }
}

Wirkung. synchronized auf einer Instanzmethode sperrt das Monitorobjekt this. Die Sequenz aus Prüfung und Änderung liegt vollständig innerhalb der Sperre, damit ist sie atomar gegenüber anderen synchronisierten Methoden desselben Kontos. Das Ergebnis ist deterministisch 200 000.

Worauf achten.

  1. Alle Zugriffe müssen synchronisiert sein; ein ungelesener Getter oder eine direkte Feldänderung von außen hebt die Invariante auf. Deshalb ist stand private und der Getter ebenfalls synchronized.
  2. Rückgabewerte müssen die Fachlogik abbilden: auszahlen meldet per boolean, ob die Deckung reichte – eine Ausnahme ist ebenfalls vertretbar, aber nicht beides gemischt.
  3. Die Sperre möglichst kurz halten; hier ist die Operation trivial, das ist unkritisch. Bei teuren Operationen (z. B. Protokollierung) sollte man sie aus dem synchronisierten Block herausziehen.
  4. synchronized ersetzt keine fachliche Invariante: stand darf nie negativ werden, das muss die Methode selbst sichern.
  5. Zum Testen eignet sich ein Zähler mit CountDownLatch und 10 000 Iterationen; ohne Synchronisation schlägt der Test zuverlässig fehl.

3. Für Überweisungen sollen beide Konten gesperrt werden. Erklären Sie die Deadlock-Gefahr und bewerten Sie zwei Gegenmaßnahmen.

Read the worked answer

Deadlock-Szenario. Thread 1 überweist von A nach B und sperrt dafür A, dann B. Thread 2 überweist von B nach A und sperrt B, dann A. Folgt die Sperrung verschachtelt, wartet Thread 1 auf B, das Thread 2 hält, und Thread 2 auf A, das Thread 1 hält – ein Zyklus im Wartegraph, beide blockieren dauerhaft. Von den vier Coffman-Bedingungen sind Mutual Exclusion, Hold-and-Wait und No-Preemption bei Monitoren kaum vermeidbar; der Zyklus ist die Bedingung, die man angreift.

Maßnahme 1: globale Sperrordnung. Beide Threads sperren Konten stets in derselben Reihenfolge, z. B. nach aufsteigender Kontonummer. Ist A < B, sperrt auch die Rücküberweisung zuerst A. Ein Zyklus ist dann ausgeschlossen, weil kein Thread auf ein Konto mit kleinerer Nummer warten kann, während er ein größeres hält.

Maßnahme 2: tryLock mit Timeout. Beide Sperren werden mit tryLock(timeout) angefordert; scheitert die zweite, wird die erste freigegeben und der Vorgang wiederholt. Das vermeidet den Deadlock, erzeugt aber Livelock-Risiko und erfordert Rückabwicklung – bei Geldbewegungen heikel.

Bewertung. Maßnahme 1 ist vorzuziehen: Sie ist deterministisch, einfach zu prüfen und kostet keine Wiederholungen. Sie muss als Invariante im Code dokumentiert und durch einen Test mit gegenläufigen Überweisungen abgesichert werden. Maßnahme 2 ist nur sinnvoll, wenn die Sperrordnung nicht durchsetzbar ist (z. B. bei fremden Komponenten). In beiden Fällen gilt: Sperren in finally freigeben und die eigentliche Buchung erst nach beiden Sperren beginnen.