Von Jamal Hassan, Backend Developer bei Java Fleet Systems Consulting
Mit Einblicken von Elyndra Valen (Senior Dev) und Nova Trent (Junior Dev)

Schwierigkeit: 🔴 Fortgeschritten
Lesezeit: 45 Minuten
Voraussetzungen: Tag 8 (Multithreading Basics)
Kurs: Java Erweiterte Techniken – Tag 9 von 10


📖 Java Erweiterte Techniken – Alle Tage

TagThemaLevel
1Collections – Listen🟢
2Collections – Sets & Maps🟢
3Generics🟡
4Lambda-Ausdrücke🟡
5Functional Interfaces🟡
6Stream-API🟡
7File I/O🟡
8Annotations & Multithreading Basics🟡
→ 9Multithreading – Synchronisation🔴
10Netzwerkprogrammierung🔴

📍 Du bist hier: Tag 9


⚡ Das Wichtigste in 30 Sekunden

Dein Problem: Mehrere Threads greifen auf dieselben Daten zu. Chaos entsteht: Race Conditions, inkonsistente Daten, schwer zu findende Bugs.

Die Lösung: Synchronisation – kontrollierter Zugriff auf gemeinsame Ressourcen.

Heute lernst du:

  • ✅ Race Conditions verstehen und vermeiden
  • synchronized Blöcke und Methoden
  • volatile für Sichtbarkeit
  • java.util.concurrent Locks
  • ✅ Atomic-Klassen für lockfreie Operationen
  • ✅ Thread-sichere Collections
  • ✅ Deadlocks erkennen und vermeiden

👋 Jamal: „Der unsichtbare Bug“

Hi! 👋

Jamal hier für den anspruchsvollsten Tag unseres Kurses. Synchronisation ist das, was Multithreading richtig schwer macht.

Das Problem:

public class Konto {
    private int kontostand = 1000;
    
    public void abheben(int betrag) {
        if (kontostand >= betrag) {
            // Thread A ist hier... wechselt zu Thread B
            kontostand -= betrag;
        }
    }
}

// Zwei Threads heben gleichzeitig 800€ ab
// Kontostand: 1000€
// Thread A prüft: 1000 >= 800? Ja!
// Thread B prüft: 1000 >= 800? Ja!  (noch nicht abgezogen!)
// Thread A: kontostand = 1000 - 800 = 200
// Thread B: kontostand = 200 - 800 = -600  // ÜBERZOGEN!

Das ist eine Race Condition – das Ergebnis hängt davon ab, welcher Thread zuerst fertig ist.


🖼️ Race Condition visualisiert

Multithreading

Abbildung 1: Zwei Threads kämpfen um dieselbe Ressource


🟢 GRUNDLAGEN

Das synchronized Keyword

Methode synchronisieren:

public class Konto {
    private int kontostand = 1000;
    
    public synchronized void abheben(int betrag) {
        if (kontostand >= betrag) {
            kontostand -= betrag;
        }
    }
    
    public synchronized void einzahlen(int betrag) {
        kontostand += betrag;
    }
    
    public synchronized int getKontostand() {
        return kontostand;
    }
}

Was passiert?

  • Nur ein Thread kann gleichzeitig eine synchronized-Methode des Objekts ausführen
  • Andere Threads warten (blockieren)
  • Lock wird automatisch freigegeben am Ende der Methode

Synchronized Block

Feingranularer als synchronized-Methoden:

public class Konto {
    private int kontostand = 1000;
    private final Object lock = new Object();
    
    public void abheben(int betrag) {
        // Code VOR dem kritischen Bereich (kann parallel laufen)
        System.out.println("Bereite Abhebung vor...");
        
        synchronized (lock) {
            // NUR DIESER BEREICH ist geschützt
            if (kontostand >= betrag) {
                kontostand -= betrag;
            }
        }
        
        // Code NACH dem kritischen Bereich
        System.out.println("Abhebung verarbeitet");
    }
}

Warum ein separates Lock-Objekt?

  • synchronized(this) lockt das ganze Objekt
  • Separates Lock = feinere Kontrolle
  • Mehrere unabhängige Locks möglich

Worauf locken?

// Auf this locken (wie synchronized-Methode)
synchronized (this) {
    // ...
}

// Auf separates Objekt locken (empfohlen)
private final Object lock = new Object();
synchronized (lock) {
    // ...
}

// Auf Klasse locken (für statische Methoden)
synchronized (Konto.class) {
    // ...
}

// Auf die Collection selbst locken
synchronized (liste) {
    liste.add(element);
}

volatile – Sichtbarkeit garantieren

public class StoppbareAufgabe implements Runnable {
    private volatile boolean running = true;
    
    public void stop() {
        running = false;  // Änderung sofort für alle Threads sichtbar
    }
    
    @Override
    public void run() {
        while (running) {
            // Arbeite...
        }
        System.out.println("Gestoppt!");
    }
}

Ohne volatile:

  • Jeder Thread hat eigenen Cache
  • Änderungen möglicherweise nicht sichtbar
  • Thread läuft endlos weiter!

Mit volatile:

  • Schreiboperationen sofort im Hauptspeicher
  • Leseoperationen immer aus Hauptspeicher
  • Aber: Keine Atomarität! count++ ist trotzdem nicht thread-safe

🟡 PROFESSIONALS

java.util.concurrent.locks

Mehr Kontrolle als synchronized:

import java.util.concurrent.locks.*;

public class Konto {
    private int kontostand = 1000;
    private final ReentrantLock lock = new ReentrantLock();
    
    public void abheben(int betrag) {
        lock.lock();  // Lock erwerben
        try {
            if (kontostand >= betrag) {
                kontostand -= betrag;
            }
        } finally {
            lock.unlock();  // IMMER in finally!
        }
    }
    
    // Mit Timeout - blockiert nicht ewig
    public boolean abhebenMitTimeout(int betrag) throws InterruptedException {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                if (kontostand >= betrag) {
                    kontostand -= betrag;
                    return true;
                }
            } finally {
                lock.unlock();
            }
        }
        return false;  // Lock nicht bekommen
    }
}

Vorteile gegenüber synchronized:

  • tryLock() – Versuch ohne Blockieren
  • tryLock(timeout) – Versuch mit Timeout
  • lockInterruptibly() – Unterbrechbar
  • Fairness-Option: new ReentrantLock(true)

ReadWriteLock – Lesen parallel, Schreiben exklusiv

import java.util.concurrent.locks.*;

public class Cache {
    private final Map<String, String> data = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    
    public String get(String key) {
        readLock.lock();  // Mehrere Leser parallel OK
        try {
            return data.get(key);
        } finally {
            readLock.unlock();
        }
    }
    
    public void put(String key, String value) {
        writeLock.lock();  // Exklusiv - keine anderen Leser/Schreiber
        try {
            data.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

Wann nutzen?

  • Viele Lesezugriffe, wenige Schreibzugriffe
  • Lesen ist teuer (z.B. Berechnung)

Atomic-Klassen – Lockfrei und schnell

import java.util.concurrent.atomic.*;

public class Zaehler {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // Thread-safe ohne Lock!
    }
    
    public int get() {
        return count.get();
    }
}

// Weitere Atomic-Klassen:
AtomicLong atomicLong = new AtomicLong(0);
AtomicBoolean atomicBool = new AtomicBoolean(false);
AtomicReference<String> atomicRef = new AtomicReference<>("initial");

// Nützliche Methoden:
int oldValue = count.getAndIncrement();  // Erst lesen, dann erhöhen
int newValue = count.incrementAndGet();  // Erst erhöhen, dann lesen
count.compareAndSet(expected, newValue); // CAS-Operation
count.updateAndGet(x -> x * 2);          // Mit Lambda

Wann Atomic statt Lock?

  • Einfache Operationen (Zähler, Flags)
  • Hohe Parallelität
  • Performance kritisch

Thread-sichere Collections

import java.util.concurrent.*;

// ===== STATT ArrayList =====
List<String> safeList = new CopyOnWriteArrayList<>();
// Gut für: Viele Leser, wenige Schreiber
// Nachteil: Kopiert bei jedem Schreiben

// ===== STATT HashSet =====
Set<String> safeSet = ConcurrentHashMap.newKeySet();
// Oder:
Set<String> safeSet2 = new CopyOnWriteArraySet<>();

// ===== STATT HashMap =====
Map<String, Integer> safeMap = new ConcurrentHashMap<>();
// Hochperformant, feinkörnige Locks

// ===== Queues =====
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("Element");        // Blockiert wenn voll
String item = queue.take();  // Blockiert wenn leer

// Mit Timeout:
queue.offer("Element", 1, TimeUnit.SECONDS);
String item2 = queue.poll(1, TimeUnit.SECONDS);

Faustregeln:

SituationEmpfehlung
Map mit vielen ZugriffenConcurrentHashMap
Liste, meist lesenCopyOnWriteArrayList
Liste, viel schreibenCollections.synchronizedList()
Producer-ConsumerBlockingQueue

wait() und notify() – Thread-Kommunikation

public class ProducerConsumer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int MAX_SIZE = 10;
    
    public synchronized void produce(int item) throws InterruptedException {
        while (queue.size() == MAX_SIZE) {
            wait();  // Warte bis Platz frei
        }
        queue.add(item);
        System.out.println("Produziert: " + item);
        notifyAll();  // Wecke wartende Consumer
    }
    
    public synchronized int consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();  // Warte bis Daten da
        }
        int item = queue.poll();
        System.out.println("Konsumiert: " + item);
        notifyAll();  // Wecke wartende Producer
        return item;
    }
}

Wichtig:

  • wait() und notify() nur in synchronized!
  • Immer in while-Schleife prüfen (spurious wakeups)
  • notifyAll() weckt alle, notify() nur einen

🔵 BONUS

Deadlocks erkennen und vermeiden

Abbildung 2: Zwei Threads blockieren sich gegenseitig

// DEADLOCK-GEFAHR!
public class DeadlockBeispiel {
    private final Object lockA = new Object();
    private final Object lockB = new Object();
    
    public void methode1() {
        synchronized (lockA) {
            System.out.println("Thread 1: hat Lock A");
            synchronized (lockB) {  // Wartet auf Lock B
                System.out.println("Thread 1: hat Lock B");
            }
        }
    }
    
    public void methode2() {
        synchronized (lockB) {  // Hat Lock B
            System.out.println("Thread 2: hat Lock B");
            synchronized (lockA) {  // Wartet auf Lock A -> DEADLOCK!
                System.out.println("Thread 2: hat Lock A");
            }
        }
    }
}

Deadlock-Vermeidung:

// LÖSUNG 1: Gleiche Lock-Reihenfolge
public void methode1() {
    synchronized (lockA) {
        synchronized (lockB) { /* ... */ }
    }
}

public void methode2() {
    synchronized (lockA) {  // Gleiche Reihenfolge: A dann B
        synchronized (lockB) { /* ... */ }
    }
}

// LÖSUNG 2: tryLock mit Timeout
public void sichereMethode() {
    while (true) {
        if (lockA.tryLock()) {
            try {
                if (lockB.tryLock()) {
                    try {
                        // Beide Locks erhalten
                        return;
                    } finally {
                        lockB.unlock();
                    }
                }
            } finally {
                lockA.unlock();
            }
        }
        Thread.sleep(10);  // Kurz warten, neu versuchen
    }
}

Semaphore – Begrenzte Ressourcen

import java.util.concurrent.Semaphore;

public class ConnectionPool {
    private final Semaphore semaphore;
    
    public ConnectionPool(int maxConnections) {
        this.semaphore = new Semaphore(maxConnections);
    }
    
    public void useConnection() throws InterruptedException {
        semaphore.acquire();  // Blockiert wenn keine Permits verfügbar
        try {
            System.out.println("Verbindung genutzt von " + Thread.currentThread().getName());
            Thread.sleep(1000);
        } finally {
            semaphore.release();  // Permit zurückgeben
        }
    }
}

// Verwendung: Max 3 gleichzeitige Verbindungen
ConnectionPool pool = new ConnectionPool(3);
for (int i = 0; i < 10; i++) {
    new Thread(() -> pool.useConnection()).start();
}

CountDownLatch – Auf mehrere Threads warten

import java.util.concurrent.CountDownLatch;

public class StartSignal {
    public static void main(String[] args) throws InterruptedException {
        int anzahlWorker = 5;
        CountDownLatch latch = new CountDownLatch(anzahlWorker);
        
        for (int i = 0; i < anzahlWorker; i++) {
            final int id = i;
            new Thread(() -> {
                System.out.println("Worker " + id + " arbeitet...");
                try { Thread.sleep(1000); } catch (InterruptedException e) {}
                System.out.println("Worker " + id + " fertig!");
                latch.countDown();  // Zähler -1
            }).start();
        }
        
        System.out.println("Warte auf alle Worker...");
        latch.await();  // Blockiert bis Zähler = 0
        System.out.println("Alle Worker fertig!");
    }
}

💬 Real Talk: Synchronisations-Fallen

Java Fleet Büro, Montag 09:00. Debugging-Session.


Nova: „Jamal, mein Zähler zeigt immer falsche Werte an!“

private int counter = 0;

public void increment() {
    counter++;  // NICHT thread-safe!
}

Jamal:counter++ ist drei Operationen: Lesen, Erhöhen, Schreiben. Dazwischen kann ein anderer Thread dazwischenfunken.“

// Lösung 1: synchronized
public synchronized void increment() {
    counter++;
}

// Lösung 2: AtomicInteger (besser!)
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
    counter.incrementAndGet();
}

Nova: „Und mein Programm hängt manchmal einfach…“

Elyndra: schaut auf den Code „Deadlock. Du lockst in unterschiedlicher Reihenfolge.“

// Thread 1: lockA -> lockB
// Thread 2: lockB -> lockA
// -> DEADLOCK!

Elyndra: „Regel: Immer in derselben Reihenfolge locken. Oder tryLock() mit Timeout verwenden.“


Jamal: „Und denkt dran: volatile macht Variablen sichtbar, aber nicht atomar!“

private volatile int count = 0;
count++;  // IMMER NOCH NICHT THREAD-SAFE!

❓ FAQ

Frage 1: synchronized Methode vs. Block?

synchronized Methode:

  • Einfacher zu lesen
  • Lockt auf this (oder Klasse bei static)
  • Ganze Methode ist kritischer Bereich

synchronized Block:

  • Feingranularer
  • Kann auf beliebiges Objekt locken
  • Nur Teil der Methode ist kritisch

Frage 2: Wann Lock, wann synchronized?

synchronizedReentrantLock
EinfacherMehr Features
Automatisch unlockManuell unlock (try-finally!)
Kein TimeouttryLock mit Timeout
Nicht unterbrechbarlockInterruptibly()

Faustregel: synchronized für einfache Fälle, Lock für komplexe Anforderungen.


Frage 3: Ist ConcurrentHashMap immer thread-safe?

Einzelne Operationen: Ja!

map.put("key", "value");  // Thread-safe
map.get("key");           // Thread-safe

Zusammengesetzte Operationen: Nein!

// NICHT thread-safe:
if (!map.containsKey("key")) {
    map.put("key", "value");
}

// Thread-safe Alternative:
map.putIfAbsent("key", "value");
map.computeIfAbsent("key", k -> "value");

Frage 4: Was ist ein „spurious wakeup“?

Ein Thread kann aus wait() aufwachen, ohne dass notify() aufgerufen wurde. Deshalb immer in while-Schleife prüfen:

// FALSCH:
if (queue.isEmpty()) {
    wait();
}

// RICHTIG:
while (queue.isEmpty()) {
    wait();
}

Frage 5: Bernd sagt, er braucht keine Synchronisation?

seufz Bernds Code läuft auf einem Kern. In Produktion mit 32 Kernen… Boom! 💥

🔍 „behind the code“ oder „in my feels“? Die echten Geschichten findest du, wenn du weißt wo du suchen musst…


🎁 Cheat Sheet

🟢 synchronized

// Methode
public synchronized void methode() { }

// Block
synchronized (lock) { }

// Auf this
synchronized (this) { }

// Auf Klasse
synchronized (MeineKlasse.class) { }

🟡 Locks

ReentrantLock lock = new ReentrantLock();
lock.lock();
try { /* ... */ } finally { lock.unlock(); }

// Mit Timeout
if (lock.tryLock(1, TimeUnit.SECONDS)) { }

// ReadWriteLock
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();   // Mehrere Leser OK
rwLock.writeLock().lock();  // Exklusiv

🔵 Atomic

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
count.getAndIncrement();
count.compareAndSet(expected, newValue);
count.updateAndGet(x -> x * 2);

🟣 Collections

ConcurrentHashMap<K, V>      // Thread-safe Map
CopyOnWriteArrayList<E>      // Viele Leser
BlockingQueue<E>             // Producer-Consumer
Collections.synchronizedList() // Wrapper

🎨 Challenge für dich!

🟢 Level 1 – Einsteiger

  • [ ] Mache einen einfachen Zähler thread-safe mit synchronized
  • [ ] Verwende AtomicInteger für einen Zähler
  • [ ] Erstelle eine thread-safe Getter/Setter Klasse

Geschätzte Zeit: 30-40 Minuten

🟡 Level 2 – Fortgeschritten

  • [ ] Implementiere einen Thread-safe Stack mit Lock
  • [ ] Nutze ReadWriteLock für einen Cache
  • [ ] Baue einen Producer-Consumer mit BlockingQueue

Geschätzte Zeit: 45-60 Minuten

🔵 Level 3 – Profi

  • [ ] Finde und behebe einen Deadlock
  • [ ] Implementiere einen ConnectionPool mit Semaphore
  • [ ] Nutze CountDownLatch für parallele Initialisierung

Geschätzte Zeit: 60-90 Minuten


📦 Downloads

ProjektFür wen?Download
tag09-synchronisation-starter.zip🟢 Mit TODOs⬇️ Download
tag09-synchronisation-complete.zip🟡 Musterlösung⬇️ Download

🔗 Weiterführende Links

🇩🇪 Deutsch

RessourceBeschreibung
Rheinwerk: ThreadsSynchronisation Kapitel

🇬🇧 Englisch

RessourceBeschreibungLevel
Oracle: SynchronizationOffizielle Doku🟡
Baeldung: java.util.concurrentPraxisbeispiele🟡
Java Concurrency in PracticeDas Standardwerk🔴

👋 Geschafft! 🎉

Was du heute gelernt hast:

✅ Race Conditions verstehen und vermeiden
✅ synchronized für gegenseitigen Ausschluss
✅ volatile für Sichtbarkeit
✅ ReentrantLock und ReadWriteLock
✅ Atomic-Klassen für lockfreie Operationen
✅ Thread-sichere Collections
✅ Deadlocks erkennen und vermeiden

Fragen? jamal.hassan@java-developer.online


📖 Weiter geht’s!

← Vorheriger Tag: Tag 8: Annotations & Multithreading Basics
→ Nächster Tag: Tag 10: Netzwerkprogrammierung


Tags: #Java #Multithreading #Synchronisation #Concurrency #ThreadSafety #Tutorial

© 2025 Java Fleet Systems Consulting | java-developer.online

Autoren

  • Jamal Hassan

    ⚙️ Jamal Hassan – Der Zuverlässige

    Backend Developer | 34 Jahre | „Ich schau mir das an.“

    Wenn im Team jemand gebraucht wird, der ruhig bleibt, während alle anderen hektisch diskutieren, dann ist es Jamal.
    Er redet nicht viel – er löst.
    Er plant wie ein Schachspieler: drei Züge im Voraus, jede Entscheidung mit Folgen bedacht.
    Seine Art zu arbeiten ist kein Sprint, sondern eine Strategie.

    Er kam 2021 zur Java Fleet, nachdem sein vorheriges Startup gescheitert war. Statt Frust hat er Gelassenheit mitgebracht – und eine Haltung, die das Team bis heute prägt: Stabilität ist keine Bremse, sondern ein Fundament.
    In einer Welt voller Hypes baut Jamal Systeme, die bleiben.

    💻 Die Tech-Seite

    Jamal ist der Inbegriff von Backend-Handwerk.
    Er liebt Architektur, die logisch ist, Datenmodelle, die Bestand haben, und Services, die einfach laufen.
    Spring Boot, REST, Kafka, Docker, DDD – das sind seine Werkzeuge, aber nicht sein Selbstverständnis.
    Er versteht Systeme als Ökosysteme: Jede Entscheidung hat Auswirkungen, jedes Modul muss sich in das Ganze einfügen.

    Er ist der Typ Entwickler, der eine halbe Stunde in Stille auf den Bildschirm schaut – und dann mit einem Satz alles löst:

    „Das Problem liegt nicht im Code. Es liegt in der Annahme.“

    Sein Code ist wie seine Persönlichkeit: still, präzise, verlässlich.
    Er dokumentiert, was nötig ist, und schreibt Tests, weil er Verantwortung ernst nimmt.
    Er hält nichts von Schnellschüssen – und noch weniger von Ausreden.

    🌿 Die menschliche Seite

    Jamal ist kein Mensch der Bühne.
    Er mag es, wenn andere glänzen – Hauptsache, das System läuft.
    Er trinkt arabischen Kaffee, spielt Schach im Verein und genießt es, wenn Dinge logisch ineinandergreifen – egal ob Code oder Leben.
    In der Kaffeeküche hört man ihn selten, aber wenn er etwas sagt, ist es meist ein Satz, der hängen bleibt.

    Im Team ist er der stille Vertraute, der Probleme anhört, bevor er sie bewertet.
    Nova nennt ihn „den Debugger in Menschengestalt“, Kat sagt: „Wenn Jamal nickt, weißt du, dass du auf der richtigen Spur bist.“
    Und Cassian beschreibt ihn als „Architekt mit Geduld und ohne Ego“.

    🧠 Seine Rolle im Team

    Jamal ist das strukturelle Rückgrat der Crew.
    Er denkt in Systemen, nicht in Features – in Verantwortlichkeiten, nicht in Ruhm.
    Wenn Projekte drohen, aus dem Ruder zu laufen, bringt er sie mit wenigen Worten und einer klaren Architektur wieder auf Kurs.
    Er ist das, was Franz-Martin „den ruhigen Hafen im Sturm“ nennt.

    ⚡ Superkraft

    Stabilität durch Denken.
    Jamal löst nicht nur technische Probleme – er beseitigt deren Ursachen.

    ☕ Motto

    „Ich schau mir das an.“
    (Und wenn er das sagt, ist das Problem so gut wie gelöst.)

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.

  • Ensign Nova Trent

    24 Jahre alt, frisch von der Universität als Junior Entwicklerin bei Java Fleet Systems Consulting. Nova ist brilliant in Algorithmen und Datenstrukturen, aber neu in der praktischen Java-Enterprise-Entwicklung. Sie brennt darauf, ihre ersten echten Projekte zu bauen und entdeckt dabei die Lücke zwischen Uni-Theorie und Entwickler-Realität. Sie liebt Star Treck das ist der Grund warum alle Sie Ensign Nova nennen und arbeitet daraufhin das sie Ihren ersten Knopf am Kragen bekommt.