Locking-und-Deadlocks-Lab

19 Java-Dateien. 7 zentrale Dateien werden direkt mit echtem Quellcode und ihrem Zusammenspiel erklärt.

Zurück zu Code-Labs

Kapitel 03 · Locking, Isolation und Deadlocks

Was dieses Lab zeigt

Simuliert konkurrierende Zugriffe ohne externe Datenbank. Optimistic und Pessimistic Locking, Isolationseffekte, Timeouts und Deadlocks werden reproduzierbar und testbar.

Lernziele

  • Lost Updates erkennen
  • Locking-Strategien vergleichen
  • Deadlocks und Timeouts behandeln

Technik und Schwerpunkte

Java 2119 Java-Dateien1 Tests/RunnerOptimistic LockingIsolationDeadlocks
Echter Quellcode aus diesem Lab

Geführter Codepfad

Der geführte Pfad vergleicht naive Zugriffe, optimistisches und pessimistisches Locking sowie Deadlock-Situationen. Jede Strategie wird an denselben Bestands- und Kontodaten sichtbar gemacht.

Run4C2DemoOptimisticInventoryServicePessimisticInventoryServiceRowLockManagerSimulatedTransactionDeadlockSimulatorRun4C2TestRunner
Lesereihenfolge der zentralen Klassen. Die Pfeile zeigen den didaktischen Weg durch den realen Quellcode, nicht zwingend jeden Laufzeitaufruf.
1. Run4C2DemoStartet die konkurrierenden Szenarien und zeigt die beobachtbaren Ergebnisse der verschiedenen Sperrstrategien.
2. OptimisticInventoryServiceReserviert Bestand über Versionsprüfung und erkennt konkurrierende Änderungen beim Schreiben.
3. PessimisticInventoryServiceSichert den kritischen Bestandszugriff über eine exklusive Zeilensperre ab.
4. RowLockManagerVerwaltet Sperren, Wartezeiten und Timeouts für pessimistische Zugriffe.
5. SimulatedTransactionSimuliert Sichtbarkeit und Leseverhalten unterschiedlicher Isolationsebenen.
6. DeadlockSimulatorErzeugt gezielt eine zyklische Sperrabhängigkeit und liefert ein auswertbares Deadlock-Ergebnis.
7. Run4C2TestRunnerVerifiziert Lost-Update-Schutz, Lock-Timeouts, Isolation und Deadlock-Erkennung.

1. Run4C2Demo

src/main/java/com/example/lockdeepdive/demo/Run4C2Demo.java
Java-Datei öffnen
Rolle im Ablauf

Startet die konkurrierenden Szenarien und zeigt die beobachtbaren Ergebnisse der verschiedenen Sperrstrategien.

Im Lesepfad folgt OptimisticInventoryService: Reserviert Bestand über Versionsprüfung und erkennt konkurrierende Änderungen beim Schreiben.

Typ
class Run4C2Demo
Verwendet
OptimisticInventoryService, PessimisticInventoryService, RowLockManager, SimulatedTransaction, DeadlockSimulator
Verwendet von
Einstiege
main(String[] args)
package com.example.lockdeepdive.demo;

import com.example.lockdeepdive.deadlock.*;
import com.example.lockdeepdive.inventory.*;
import com.example.lockdeepdive.isolation.*;
import com.example.lockdeepdive.locking.*;
import com.example.lockdeepdive.shared.*;
import java.time.Duration;

public final class Run4C2Demo {
    public static void main(String[] args) {
        ProductId product = ProductId.of("SKU-LOCK-1");
        InventoryTable table = new InventoryTable();
        table.insert(product, 10);

        NaiveReservationSession a = new NaiveReservationSession(table, table.read(product));
        NaiveReservationSession b = new NaiveReservationSession(table, table.read(product));
        a.planReservation(7);
        b.planReservation(6);
        a.commitWithoutVersionCheck();
        b.commitWithoutVersionCheck();
        System.out.println("LOST_UPDATE_FINAL_QUANTITY=" + table.quantity(product));

        InventoryTable optimisticTable = new InventoryTable();
        optimisticTable.insert(product, 10);
        OptimisticInventoryService optimistic = new OptimisticInventoryService(optimisticTable, 3);
        System.out.println("OPTIMISTIC_FIRST=" + optimistic.reserve(product, 7).message());
        System.out.println("OPTIMISTIC_SECOND=" + optimistic.reserve(product, 6).message());

        InventoryTable pessimisticTable = new InventoryTable();
        pessimisticTable.insert(product, 10);
        PessimisticInventoryService pessimistic = new PessimisticInventoryService(pessimisticTable, new RowLockManager(), Duration.ofMillis(300));
        System.out.println("PESSIMISTIC=" + pessimistic.reserve(product, 5).message());

        AccountTable accounts = new AccountTable();
        AccountId account = AccountId.of("A-100");
        accounts.insert(account, 100);
        SimulatedTransaction readCommitted = new SimulatedTransaction(accounts, IsolationLevel.READ_COMMITTED);
        System.out.println("RC_READ_1=" + readCommitted.read(account).balance());
        accounts.update(account, 200);
        System.out.println("RC_READ_2=" + readCommitted.read(account).balance());

        DeadlockSimulationResult deadlock = new DeadlockSimulator().simulateOppositeLockOrder(Duration.ofMillis(200));
        System.out.println("DEADLOCK_TIMEOUT=" + deadlock.deadlockWasBrokenByTimeout());
    }
}

2. OptimisticInventoryService

src/main/java/com/example/lockdeepdive/inventory/OptimisticInventoryService.java
Java-Datei öffnen
Rolle im Ablauf

Reserviert Bestand über Versionsprüfung und erkennt konkurrierende Änderungen beim Schreiben.

Im Lesepfad folgt PessimisticInventoryService: Sichert den kritischen Bestandszugriff über eine exklusive Zeilensperre ab.

Typ
class OptimisticInventoryService
Verwendet
Verwendet von
Run4C2Demo, Run4C2TestRunner
Einstiege
reserve(ProductId productId, int requested)
Application Service
package com.example.lockdeepdive.inventory;

import com.example.lockdeepdive.shared.*;

// Pattern: Application Service - kapselt Use-Case-Regeln und technische Konfliktbehandlung.
public final class OptimisticInventoryService {
    private final InventoryTable table;
    private final int maxRetries;

    public OptimisticInventoryService(InventoryTable table, int maxRetries) {
        this.table = table;
        this.maxRetries = maxRetries;
    }

    public ReservationResult reserve(ProductId productId, int requested) {
        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            StockSnapshot current = table.read(productId);
            if (current.quantity() < requested) {
                return new ReservationResult.Rejected(productId, requested, current.quantity());
            }
            int newQuantity = current.quantity() - requested;
            try {
                StockSnapshot saved = table.writeIfVersionMatches(productId, current.version(), newQuantity);
                return new ReservationResult.Reserved(productId, requested, saved.quantity(), saved.version());
            } catch (OptimisticLockException conflict) {
                if (attempt == maxRetries) {
                    return new ReservationResult.Conflict(productId, conflict.getMessage());
                }
                // In echter Produktion: kleiner Backoff + Metrik + erneute fachliche Pruefung.
            }
        }
        return new ReservationResult.Conflict(productId, "retry loop exhausted");
    }
}

3. PessimisticInventoryService

src/main/java/com/example/lockdeepdive/locking/PessimisticInventoryService.java
Java-Datei öffnen
Rolle im Ablauf

Sichert den kritischen Bestandszugriff über eine exklusive Zeilensperre ab.

Im Lesepfad folgt RowLockManager: Verwaltet Sperren, Wartezeiten und Timeouts für pessimistische Zugriffe.

Typ
class PessimisticInventoryService
Verwendet
RowLockManager
Verwendet von
Run4C2Demo, Run4C2TestRunner
Einstiege
reserve(ProductId productId, int requested)
Application Service + Pessimistic Lock
package com.example.lockdeepdive.locking;

import com.example.lockdeepdive.inventory.*;
import com.example.lockdeepdive.shared.*;
import java.time.Duration;

// Pattern: Application Service + Pessimistic Lock - kurze kritische Sektion, keine externen API-Aufrufe im Lock.
public final class PessimisticInventoryService {
    private final InventoryTable table;
    private final RowLockManager locks;
    private final Duration timeout;

    public PessimisticInventoryService(InventoryTable table, RowLockManager locks, Duration timeout) {
        this.table = table;
        this.locks = locks;
        this.timeout = timeout;
    }

    public ReservationResult reserve(ProductId productId, int requested) {
        try (RowLockManager.Guard ignored = locks.lock(productId, timeout)) {
            StockSnapshot current = table.read(productId);
            if (current.quantity() < requested) {
                return new ReservationResult.Rejected(productId, requested, current.quantity());
            }
            StockSnapshot saved = table.writeIfVersionMatches(productId, current.version(), current.quantity() - requested);
            return new ReservationResult.Reserved(productId, requested, saved.quantity(), saved.version());
        } catch (LockTimeoutException timeout) {
            return new ReservationResult.Conflict(productId, timeout.getMessage());
        }
    }
}

4. RowLockManager

src/main/java/com/example/lockdeepdive/locking/RowLockManager.java
Java-Datei öffnen
Rolle im Ablauf

Verwaltet Sperren, Wartezeiten und Timeouts für pessimistische Zugriffe.

Im Lesepfad folgt SimulatedTransaction: Simuliert Sichtbarkeit und Leseverhalten unterschiedlicher Isolationsebenen.

Typ
class RowLockManager
Verwendet
Verwendet von
Run4C2Demo, PessimisticInventoryService, Run4C2TestRunner
Einstiege
lock(ProductId productId, Duration timeout), Guard(ProductId productId, ReentrantLock lock)
Lock Manager
package com.example.lockdeepdive.locking;

import com.example.lockdeepdive.shared.ProductId;
import java.time.Duration;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

// Pattern: Lock Manager - simuliert DB-Zeilensperren mit Timeout.
public final class RowLockManager {
    private final ConcurrentMap<ProductId, ReentrantLock> locks = new ConcurrentHashMap<>();

    public Guard lock(ProductId productId, Duration timeout) {
        ReentrantLock lock = locks.computeIfAbsent(productId, id -> new ReentrantLock());
        try {
            if (!lock.tryLock(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
                throw new LockTimeoutException("timeout while waiting for lock on " + productId);
            }
            return new Guard(productId, lock);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new LockTimeoutException("interrupted while waiting for lock on " + productId);
        }
    }

    public record Guard(ProductId productId, ReentrantLock lock) implements AutoCloseable {
        @Override public void close() { lock.unlock(); }
    }
}

5. SimulatedTransaction

src/main/java/com/example/lockdeepdive/isolation/SimulatedTransaction.java
Java-Datei öffnen
Rolle im Ablauf

Simuliert Sichtbarkeit und Leseverhalten unterschiedlicher Isolationsebenen.

Im Lesepfad folgt DeadlockSimulator: Erzeugt gezielt eine zyklische Sperrabhängigkeit und liefert ein auswertbares Deadlock-Ergebnis.

Typ
class SimulatedTransaction
Verwendet
Verwendet von
Run4C2Demo, Run4C2TestRunner
Einstiege
read(AccountId id), isolationLevel()
Unit of Work Mental Model
package com.example.lockdeepdive.isolation;

import java.util.*;

// Pattern: Unit of Work Mental Model - Transaktion merkt sich je nach Isolation gelesene Snapshots.
public final class SimulatedTransaction {
    private final AccountTable table;
    private final IsolationLevel isolationLevel;
    private final Map<AccountId, AccountSnapshot> repeatableReadCache = new HashMap<>();

    public SimulatedTransaction(AccountTable table, IsolationLevel isolationLevel) {
        this.table = table;
        this.isolationLevel = isolationLevel;
    }

    public AccountSnapshot read(AccountId id) {
        if (isolationLevel == IsolationLevel.REPEATABLE_READ) {
            return repeatableReadCache.computeIfAbsent(id, table::committedRead);
        }
        return table.committedRead(id);
    }

    public IsolationLevel isolationLevel() { return isolationLevel; }
}

6. DeadlockSimulator

src/main/java/com/example/lockdeepdive/deadlock/DeadlockSimulator.java
Java-Datei öffnen
Rolle im Ablauf

Erzeugt gezielt eine zyklische Sperrabhängigkeit und liefert ein auswertbares Deadlock-Ergebnis.

Im Lesepfad folgt Run4C2TestRunner: Verifiziert Lost-Update-Schutz, Lock-Timeouts, Isolation und Deadlock-Erkennung.

Typ
class DeadlockSimulator
Verwendet
Verwendet von
Run4C2Demo, Run4C2TestRunner
Einstiege
simulateOppositeLockOrder(Duration timeout)
Deterministic Concurrency Test Helper
package com.example.lockdeepdive.deadlock;

import java.time.Duration;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;

// Pattern: Deterministic Concurrency Test Helper - erzwingt gegensaetzliche Lock-Reihenfolge reproduzierbar.
public final class DeadlockSimulator {
    private final ReentrantLock productOne = new ReentrantLock();
    private final ReentrantLock productTwo = new ReentrantLock();

    public DeadlockSimulationResult simulateOppositeLockOrder(Duration timeout) {
        CountDownLatch bothHoldFirstLock = new CountDownLatch(2);
        ExecutorService executor = Executors.newFixedThreadPool(2);
        try {
            Future<Boolean> first = executor.submit(() -> worker(productOne, productTwo, bothHoldFirstLock, timeout));
            Future<Boolean> second = executor.submit(() -> worker(productTwo, productOne, bothHoldFirstLock, timeout));
            boolean firstTimedOut = first.get(5, TimeUnit.SECONDS);
            boolean secondTimedOut = second.get(5, TimeUnit.SECONDS);
            return new DeadlockSimulationResult(firstTimedOut, secondTimedOut,
                    "opposite lock order created a wait cycle; timeout broke at least one worker");
        } catch (Exception e) {
            throw new IllegalStateException("deadlock simulation failed", e);
        } finally {
            executor.shutdownNow();
        }
    }

    private boolean worker(ReentrantLock first, ReentrantLock second, CountDownLatch latch, Duration timeout) throws Exception {
        first.lock();
        try {
            latch.countDown();
            if (!latch.await(2, TimeUnit.SECONDS)) throw new IllegalStateException("test setup failed");
            boolean acquiredSecond = second.tryLock(timeout.toMillis(), TimeUnit.MILLISECONDS);
            if (!acquiredSecond) return true;
            try { return false; } finally { second.unlock(); }
        } finally {
            first.unlock();
        }
    }
}

7. Run4C2TestRunner

src/test/java/com/example/lockdeepdive/Run4C2TestRunner.java
Java-Datei öffnen
Rolle im Ablauf

Verifiziert Lost-Update-Schutz, Lock-Timeouts, Isolation und Deadlock-Erkennung.

Damit ist der zentrale Pfad abgeschlossen; der Test-/Runner-Code und die vollständige Dateiliste darunter zeigen die übrigen Varianten.

Typ
class Run4C2TestRunner
Verwendet
OptimisticInventoryService, PessimisticInventoryService, RowLockManager, SimulatedTransaction, DeadlockSimulator
Verwendet von
Einstiege
main(String[] args)
package com.example.lockdeepdive;

import com.example.lockdeepdive.deadlock.*;
import com.example.lockdeepdive.inventory.*;
import com.example.lockdeepdive.isolation.*;
import com.example.lockdeepdive.locking.*;
import com.example.lockdeepdive.shared.*;
import java.time.Duration;

public final class Run4C2TestRunner {
    public static void main(String[] args) {
        lostUpdateIsVisibleWithoutVersionCheck();
        optimisticLockDetectsStaleWrite();
        optimisticServiceRechecksBusinessRuleAfterConflict();
        pessimisticLockSerializesCriticalSection();
        readCommittedAllowsNonRepeatableRead();
        repeatableReadKeepsStableSnapshot();
        deadlockIsBrokenByTimeout();
        System.out.println("RUN4C2_TESTS_OK");
    }

    static void lostUpdateIsVisibleWithoutVersionCheck() {
        ProductId sku = ProductId.of("SKU-1");
        InventoryTable table = new InventoryTable();
        table.insert(sku, 10);
        NaiveReservationSession txA = new NaiveReservationSession(table, table.read(sku));
        NaiveReservationSession txB = new NaiveReservationSession(table, table.read(sku));
        assertTrue(txA.planReservation(7), "txA can reserve 7");
        assertTrue(txB.planReservation(6), "txB can reserve 6 based on stale snapshot");
        txA.commitWithoutVersionCheck();
        txB.commitWithoutVersionCheck();
        assertEquals(4, table.quantity(sku), "last writer wins although 13 items were promised from stock 10");
    }

    static void optimisticLockDetectsStaleWrite() {
        ProductId sku = ProductId.of("SKU-2");
        InventoryTable table = new InventoryTable();
        table.insert(sku, 10);
        StockSnapshot a = table.read(sku);
        StockSnapshot b = table.read(sku);
        table.writeIfVersionMatches(sku, a.version(), 3);
        assertThrows(OptimisticLockException.class, () -> table.writeIfVersionMatches(sku, b.version(), 4),
                "stale writer must be rejected");
    }

    static void optimisticServiceRechecksBusinessRuleAfterConflict() {
        ProductId sku = ProductId.of("SKU-3");
        InventoryTable table = new InventoryTable();
        table.insert(sku, 10);
        OptimisticInventoryService service = new OptimisticInventoryService(table, 3);
        ReservationResult first = service.reserve(sku, 7);
        ReservationResult second = service.reserve(sku, 6);
        assertTrue(first.success(), "first reservation succeeds");
        assertTrue(!second.success(), "second reservation is rejected after reread because only 3 remain");
    }

    static void pessimisticLockSerializesCriticalSection() {
        ProductId sku = ProductId.of("SKU-4");
        InventoryTable table = new InventoryTable();
        table.insert(sku, 10);
        PessimisticInventoryService service = new PessimisticInventoryService(table, new RowLockManager(), Duration.ofMillis(500));
        ReservationResult first = service.reserve(sku, 6);
        ReservationResult second = service.reserve(sku, 5);
        assertTrue(first.success(), "first reservation succeeds");
        assertTrue(!second.success(), "second reservation sees updated remaining stock and fails fachlich");
        assertEquals(4, table.quantity(sku), "stock remains consistent");
    }

    static void readCommittedAllowsNonRepeatableRead() {
        AccountId account = AccountId.of("A-1");
        AccountTable table = new AccountTable();
        table.insert(account, 100);
        SimulatedTransaction tx = new SimulatedTransaction(table, IsolationLevel.READ_COMMITTED);
        int first = tx.read(account).balance();
        table.update(account, 150);
        int second = tx.read(account).balance();
        assertEquals(100, first, "first read");
        assertEquals(150, second, "second read sees newer committed value");
    }

    static void repeatableReadKeepsStableSnapshot() {
        AccountId account = AccountId.of("A-2");
        AccountTable table = new AccountTable();
        table.insert(account, 100);
        SimulatedTransaction tx = new SimulatedTransaction(table, IsolationLevel.REPEATABLE_READ);
        int first = tx.read(account).balance();
        table.update(account, 150);
        int second = tx.read(account).balance();
        assertEquals(100, first, "first read");
        assertEquals(100, second, "repeatable read keeps snapshot stable inside transaction");
    }

    static void deadlockIsBrokenByTimeout() {
        DeadlockSimulationResult result = new DeadlockSimulator().simulateOppositeLockOrder(Duration.ofMillis(200));
        assertTrue(result.deadlockWasBrokenByTimeout(), "timeout should break wait cycle");
    }

    static void assertTrue(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
    static void assertEquals(int expected, int actual, String message) { if (expected != actual) throw new AssertionError(message + ": expected=" + expected + ", actual=" + actual); }
    static void assertThrows(Class<? extends Throwable> type, Runnable action, String message) {
        try { action.run(); }
        catch (Throwable t) { if (type.isInstance(t)) return; throw new AssertionError(message + ": wrong exception " + t); }
        throw new AssertionError(message + ": expected exception " + type.getSimpleName());
    }
}
Alle Projektdateien öffnen (23 Einträge)
⌂ Cockpit