RU | EN | DE

Lebenszyklus eines Threads

In Java wird Multithreading durch das grundlegende Konzept des Threads definiert. Während ihres Lebenszyklus durchlaufen Threads verschiedene Zustände:

NEW - ein neu erstellter Thread, der noch nicht gestartet wurde
RUNNABLE - entweder läuft er oder ist bereit zur Ausführung, wartet aber auf Ressourcenzuweisung
BLOCKED - wartet darauf, eine Monitor-Sperre zu erhalten, um einen synchronisierten Block/Methode zu betreten oder wieder zu betreten
WAITING - wartet darauf, dass ein anderer Thread eine bestimmte Aktion ausführt, ohne Zeitbegrenzung
TIMED_WAITING - wartet darauf, dass ein anderer Thread eine bestimmte Aktion für einen festgelegten Zeitraum ausführt
TERMINATED - hat die Ausführung beendet.

Open: Pasted image 20251121132137.png

Runnable vs Extending a Thread

Einfach ausgedrückt, bevorzugen wir die Verwendung von Runnable anstelle von Thread:

Indem wir die Thread-Klasse erweitern, überschreiben wir keine ihrer Methoden. Stattdessen überschreiben wir die Runnable-Methode (die, wie sich herausstellt, von Thread implementiert wird). Dies ist eine klare Verletzung des IS-A Thread-Prinzips
Die Erstellung einer Runnable-Implementierung und deren Übergabe an die Thread-Klasse verwendet Komposition anstelle von Vererbung, was flexibler ist.
Ab Java 8 können Runnables als Lambda-Ausdrücke dargestellt werden.

Runnable vs. Callable

Beide Schnittstellen sind dazu gedacht, eine Aufgabe darzustellen, die von mehreren Threads ausgeführt werden kann. Runnable-Aufgaben können mit der Thread-Klasse oder ExecutorService ausgeführt werden, während Callables nur mit letzterem ausgeführt werden können.

wait(), notify(), notifyAll()

  • wait() — der Thread wartet und gibt den Monitor frei.
  • notify() — weckt einen Thread auf, der auf diesem Objekt wartet.
  • notifyAll() — weckt alle auf.

Synchronized

Wir können das synchronized-Schlüsselwort auf verschiedenen Ebenen verwenden:

  1. Instanzmethoden
  2. Statische Methoden
  3. Codeblöcke

Wenn wir einen synchronisierten Block verwenden, verwendet Java intern einen Monitor, auch bekannt als Monitor-Sperre oder intrinsische Sperre, um Synchronisation zu gewährleisten. Diese Monitore sind an Objekte gebunden, sodass alle synchronisierten Blöcke desselben Objekts nur von einem Thread gleichzeitig ausgeführt werden können.

Statische Methoden werden genauso synchronisiert wie Instanzmethoden.
Diese Methoden werden auf dem Class-Objekt synchronisiert, das mit der Klasse verbunden ist. Da es in der JVM nur ein Class-Objekt pro Klasse gibt, kann innerhalb einer statischen synchronisierten Methode für eine Klasse nur ein Thread ausgeführt werden, unabhängig von der Anzahl ihrer Instanzen.

Manchmal möchten wir nicht die gesamte Methode synchronisieren, sondern nur einige Anweisungen darin. Dies kann erreicht werden, indem synchronized auf einen Block angewendet wird:

public void exampleSyncBlock() {  
    synchronized(this) {    
        setCount(getCount() + 1)  
    }
}

Beachten Sie, dass wir dem synchronisierten Block den Parameter this übergeben haben. Dies ist das Monitor-Objekt. Der Code innerhalb des Blocks wird auf dem Monitor-Objekt synchronisiert. Einfach ausgedrückt kann innerhalb dieses Codeblocks nur ein Thread pro Monitor-Objekt ausgeführt werden.

Wenn die Methode statisch wäre, würden wir anstelle der Objektreferenz den Klassennamen übergeben, und die Klasse würde zum Monitor für die Synchronisation des Blocks werden.

Volatile

Wir können volatile verwenden, um Cache-Konsistenzprobleme zu lösen.

Damit Aktualisierungen von Variablen vorhersehbar an andere Threads weitergegeben werden, muss der volatile-Modifikator auf sie angewendet werden.

So können wir mit der Laufzeitumgebung und dem Prozessor kommunizieren, um Anweisungen, die mit der volatile-Variable verbunden sind, nicht neu zu ordnen. Darüber hinaus verstehen Prozessoren, dass sie alle Aktualisierungen dieser Variablen sofort löschen müssen.

volatile - ist ein sehr nützliches Schlüsselwort, da es verwendet werden kann, um die Sichtbarkeit von Datenänderungen zu gewährleisten, ohne gegenseitigen Ausschluss zu bieten. Daher ist es in Fällen nützlich, in denen es uns nichts ausmacht, dass ein Codeblock von mehreren Threads gleichzeitig ausgeführt wird, aber wir die Sichtbarkeitseigenschaft sicherstellen müssen.

Technisch gesehen erfolgt jede Schreiboperation in ein volatile-Feld vor jedem nachfolgenden Lesevorgang desselben Feldes. Dies ist die Regel der volatile-Variable des Java-Speichermodells (JMM).

Happens-before

Happens-before — ist eine grundlegende Regel des Java-Speichermodells, die definiert, welche Änderungen eines Threads garantiert von einem anderen Thread gesehen werden.

Kurz gesagt:

Wenn Aktion A happens-before Aktion B, dann sind alle Effekte von A (Schreiben in den Speicher) garantiert für den Thread sichtbar, der B ausführt.

Keine Wettlaufbedingungen, keine “schwebenden Werte” — Sichtbarkeit und Reihenfolge der Aktionen sind garantiert.

Memory Visibility

In einer Multithread-Umgebung kann jeder Thread mit seinen eigenen Datenkopien arbeiten, die im CPU-Kern-Cache liegen.
Wenn ein Thread Variablen aktualisiert, gibt es keine Garantie, dass ein anderer Thread diese Änderungen sofort im Hauptspeicher sieht.

Aufgrund von Prozessor- und Compiler-Optimierungen kann der lesende Thread:

  • den alten Wert sehen,
  • den neuen, aber mit Verzögerung,
  • Werte in einer anderen Reihenfolge sehen, als sie im Code geschrieben wurden. Deshalb geben gewöhnliche Variablen ohne Synchronisation (synchronized, volatile, locks) keine Sichtbarkeitsgarantien.

Locks

Arten von Locks

  • synchronized-Block: Dies ist ein Schlüsselwort in Java, das verwendet werden kann, um den Zugriff auf Code auf nur einen Thread zu beschränken. Dieser Block kann verwendet werden, um Methoden oder Codeblöcke zu synchronisieren.
  • ReentrantLock: Dies ist eine Klasse, die die Lock-Schnittstelle implementiert und flexiblere Sperrmöglichkeiten bietet. Es ermöglicht komplexere Sperr-Szenarien, wie Sperren mit zeitgebundenem Warten, Sperren mit Interrupt-Versuch und andere.
  • ReadWriteLock: Dies ist eine Schnittstelle, die zwei Sperren bereitstellt - eine zum Lesen und eine zum Schreiben. Dieses Schema ermöglicht es mehreren Threads, Lesevorgänge gleichzeitig auszuführen, blockiert aber den Schreibzugriff während der Datenaktualisierung.
  • StampedLock: Lock mit Unterstützung für write/read und ultra-fast optimistic read, das das Lesen beschleunigt und Contention in Multithreading reduziert.

Deadlock vs Livelock

Deadlock (Gegenseitige Blockierung)

Threads blockieren sich gegenseitig für immer, jeder wartet auf eine Ressource, die vom anderen gehalten wird.
Ergebnis: Das System steht still, es gibt keinen Fortschritt.

Beispiel:
Thread A hält Ressource 1 und wartet auf Ressource 2.
Thread B hält Ressource 2 und wartet auf Ressource 1.

Wie vermeiden:

  1. Ressourcen immer in derselben Reihenfolge sperren.
  2. Timeouts beim Erfassen von Sperren verwenden (tryLock(timeout)).
  3. Die Anzahl der gleichzeitig erfassten Sperren minimieren.

Livelock (Belebte Blockierung)

Threads sind nicht blockiert, aber bewegen sich nutzlos, versuchen ständig, Konflikte zu vermeiden und behindern sich gegenseitig.
Ergebnis: Das System arbeitet, aber es gibt auch keinen Fortschritt.

Beispiel:
Zwei Threads geben sich gegenseitig eine Ressource nach, lehnen ab und versuchen es erneut, aber synchron und endlos.

Wie vermeiden:

  1. Zufällige Verzögerungen oder exponentiellen Backoff bei wiederholten Versuchen hinzufügen.
  2. Explizite Timeouts verwenden und Versuche nach einer bestimmten Zeit beenden.
  3. Den Kooperationsalgorithmus überdenken, um sich nicht kontinuierlich gegenseitig zu blockieren.

Atomics

Atomics in Java funktionieren als Operationen, die Atomarität und Sichtbarkeit von Änderungen in gemeinsam genutzten Variablen für Multithread-Programme garantieren. Atomics in Java werden durch Klassen aus dem Paket java.util.concurrent.atomic implementiert

Atomare Operationen garantieren, dass die Operation vollständig ausgeführt wird und kein anderer Thread den Wert der Variable ändern kann, bis die Operation abgeschlossen ist.

Atomics verwenden keine Sperren. Sie gewährleisten Atomarität durch CAS — eine spezielle atomare Prozessorinstruktion, die es ermöglicht, einen Wert nur dann zu aktualisieren, wenn niemand ihn zwischen Lesen und Schreiben geändert hat.

CountDownLatch

CountDownLatch (erschien in JDK 5) - ist eine Utility-Klasse, die eine Reihe von Threads blockiert, bis eine bestimmte Operation abgeschlossen ist.

CountDownLatch wird mit einem Zähler (Integer-Typ) initialisiert, der abnimmt, wenn abhängige Threads die Ausführung beenden. Sobald der Zähler jedoch Null erreicht, werden andere Threads freigegeben.

CyclicBarrier

CyclicBarrier funktioniert praktisch genauso wie CountDownLatch, außer dass wir es wiederverwenden können. Im Gegensatz zu CountDownLatch ermöglicht es mehreren Threads, mit der await()-Methode aufeinander zu warten (bekannt als Barrierebedingung), bevor die endgültige Aufgabe aufgerufen wird.

Semaphore

Ein Semaphore wird verwendet, um den Zugriff auf Thread-Ebene auf einen Teil einer physischen oder logischen Ressource zu blockieren. Ein Semaphore enthält eine Reihe von Genehmigungen; wann immer ein Thread versucht, in einen kritischen Abschnitt einzutreten, muss er das Semaphore auf Vorhandensein oder Fehlen einer Genehmigung überprüfen.

Phaser

Phaser ist eine flexiblere Lösung als CyclicBarrier und CountDownLatch - es wird als wiederverwendbare Barriere verwendet, an der eine dynamische Anzahl von Threads warten muss, bevor die Ausführung fortgesetzt wird. Wir können mehrere Ausführungsphasen koordinieren, indem wir die Phaser-Instanz für jede Programmphase wiederverwenden.

Concurrent Collections

Dies sind Sammlungen aus java.util.concurrent, die für Multithread-Zugriff ohne externe Synchronisation sicher sind.

Beispiele:

  • ConcurrentHashMap — Thread-sichere Map, unterstützt schnelle Lese- und Schreiboperationen.
  • CopyOnWriteArrayList — Thread-sichere Liste, die das interne Array bei Modifikation kopiert.

ConcurrentHashMap wird oft für konsistenten Cache verwendet: Mehrere Threads können Daten gleichzeitig lesen und aktualisieren, während die Map in einem korrekten Zustand bleibt. Dies ist besonders praktisch zum Speichern von Zwischen- oder berechneten Werten, auf die mehrere Threads häufig zugreifen.

Executor

Dies ist eine Schnittstelle, die ein Objekt darstellt, das gestellte Aufgaben ausführt.

Von der konkreten Implementierung hängt ab, ob die Aufgabe in einem neuen oder aktuellen Thread ausgeführt wird. Mit dieser Schnittstelle können wir die Thread-Ausführung der Aufgabe vom eigentlichen Ausführungsmechanismus trennen.

Es ist zu beachten, dass Executor nicht erfordert, dass die Aufgabenausführung asynchron ist. Im einfachsten Fall kann ein Executor die vorgelegte Aufgabe sofort im aufrufenden Thread aufrufen. Wenn der Executor-Executor eine Aufgabe nicht zur Ausführung annehmen kann, wirft er eine Meldung RejectedExecutionException

ExecutorService

ExecutorService stellt eine umfassende Lösung für asynchrone Verarbeitung dar. Es verwaltet eine In-Memory-Warteschlange und plant die Ausführung von Jobs abhängig von der Thread-Verfügbarkeit:

ExecutorService excecutor = Executors.newFixedThreadPool(10);excecutor.submit(() -> {    
	//Aufgabe executor
})

Future

Future repräsentiert das zukünftige Ergebnis einer asynchronen Berechnung. Dieses Ergebnis wird schließlich im Future erscheinen, nachdem die Verarbeitung abgeschlossen ist.
Darüber hinaus bricht die cancel(boolean mayInterruptIfRunning)-API die Operation ab und gibt den ausführenden Thread frei. Wenn der Wert des mayInterruptIfRunning-Parameters true ist, wird der Thread, der die Aufgabe ausführt, sofort beendet.

CompletableFuture

Dies ist eine Erweiterung der Future-Klasse, die eine flexiblere Arbeit mit asynchronen Operationen ermöglicht. CompletableFuture ermöglicht es, den Abschluss von Operationen explizit zu steuern, mehrere Operationen in eine Kette zu kombinieren und zusätzliche Aktionen bei deren Abschluss auszuführen.

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {    // Ausführung einer asynchronen Operation    return "Ergebnis der Operation";});

Schlüsselpunkte:

thenApply — wendet eine Funktion auf das Ergebnis der vorherigen Aufgabe an, gibt einen neuen CompletableFuture zurück.

cf.thenApply(result -> result * 2);

thenCompose — entfaltet einen verschachtelten CompletableFuture, wird für sequentielle asynchrone Ausführung verwendet.

cf.thenCompose(result -> asyncTask(result));

allOf / anyOf — kombinieren mehrere CompletableFuture:

  • allOf — wartet auf den Abschluss aller Aufgaben.
  • anyOf — gibt das Ergebnis der ersten abgeschlossenen Aufgabe zurück.
CompletableFuture.allOf(cf1, cf2).join();
CompletableFuture.anyOf(cf1, cf2).thenAccept(System.out::println);

Asynchrone Ausführung — Aufgaben können in einem separaten Thread-Pool gestartet werden:

CompletableFuture.supplyAsync(() -> compute(), executor);

ForkJoinPool

Zentraler Teil des fork/join-Frameworks (Java 7). Ermöglicht die effiziente Ausführung rekursiver Aufgaben, ohne für jede Teilaufgabe einen separaten Thread zu erstellen. Aufgaben können forked (in Teilaufgaben aufgeteilt) und joined (auf deren Abschluss warten) werden. Durch den work-stealing-Algorithmus verteilen Threads Arbeit untereinander, was die Effizienz erhöht.

Empfehlungen

  1. Bitten Sie vor dem Interview ChatGPT oder suchen Sie im Internet nach Code-Review-Aufgaben für Multithreading. Oft werden in Interviews Aufgaben gestellt, bei denen es darum geht, den aktuellen Code zu überprüfen, Fehler zu finden und zu korrigieren.
  2. Üben Sie, kleine Beispiele mit ExecutorService, CompletableFuture, CountDownLatch und Semaphore zu schreiben. In Interviews wird oft gebeten zu erklären, wie Code funktioniert, und Praxis mit realen Beispielen hilft, sich schnell zu orientieren.

Fazit

Heute haben wir die Schlüsselthemen des Multithreading behandelt, die am häufigsten in Java-Vorstellungsgesprächen vorkommen. Natürlich wird nicht unbedingt alles aus dieser Liste gefragt, aber damit bleiben Sie sicherlich nicht „außen vor”. Ich habe es basierend auf meiner eigenen Erfahrung bei Vorstellungsgesprächen zusammengestellt — all dies wurde wirklich gefragt und wird weiterhin gefragt.