Dieser Artikel ist eine Übersicht der prominentesten Multithreadingmechanismen aus Java 25. Es wird aufgeführt, in welchem Szenario man welchen Mechanismus wählen sollte und es werden ein paar grundlegende Fragen beantwortet.

Der Artikel wiederholt einzelne Aspekte der Serie in knapper Form. Wenn du noch nicht mit Multithreading gearbeitet, und die Serie noch nicht gelesen hast, empfehle ich dir sehr, alles, von der Einführung an, zu lesen. Danach wirst du ein ausführliches Verständnis für alle verschiedenen Mechanismen haben. Ansonsten sind hier noch einmal die einzelnen Artikel:

Inhaltsverzeichnis

  1. OS-Threads
  2. Javas Platformthreads
  3. Einfache Executors
  4. ExecutorServices & Interrupts
  5. Threadpools
  6. (Completable) Futures
  7. Fork-Join Framework
  8. Virtual Threads
  9. Structured Concurrency

Mechanismen und Use-Cases

Executors.newVirtualThreadPerTaskExecutor()

Für viele kurzlebige, unabhängige IO-Tasks, die keine Koordination brauchen.

  • Senden von Benachrichtigungen (Mails / Push / etc…)
  • Senden von unabhängigen, schreibenden HTTP-Anfragen: HttpClient.newBuilder().executor(Executors.newVirtualThreadPerTaskExecutor())
  • Empfangen von unabhängigen Datei-Downloads
  • Tätigen von einzelnen, unabhängigen, schreibenden Statements auf einer Datenbank
  • Schreiben von Logs
  • WebSocket-Kommunikation
  • DNS-Lookups

StructuredTaskScope

Für mehrere IO-Tasks, deren Ergebnisse zusammengehören.

  • Senden von verschiedenen API-Anfragen, deren Ergebnisse miteinander zutun haben
  • Tätigen von mehreren Datenbankanfragen auf verschiedenen Tabellen, deren Ergebnisse alle benötigt werden
  • Synchronisation mehrerer Microservices

Parallele Streams

Für das Durchführen von unabhängigen CPU-Tasks auf riesigen Datenmengen.
(Mindestens zehntausende Elemente)

CompletableFutures

Verketten und Kombinieren von abhängigen asynchronen Aufgabensträngen. (CPU & IO)
Übergebe hier abhängig von den Aufgaben deinen eigenen ExecutorService.

  • CSV hochladen:
    Validieren -> In Datenbank schreiben -> Benachrichtigung senden
  • Kombinieren von parallelen Ergebnissen:
    Nutzerprofil = Social-Media-Daten + Eigene Datenbank + Daten bei Partner
  • Fallback-Strategien:
    Preis von Anbieter A holen, wenn fehlgeschlagen -> Anbieter B anfragen

Executors.newSingleThreadScheduledExecutor()

Für das Anstoßen von periodische Hintergrundaufgaben.

  • Wiederholtes Polling einer API
  • Löschen alter Logdateien
  • Durchführen von Datenbankbackups
  • Healthchecks von Services

ForkJoinTasks

Für extrem große Aufgaben, welche sehr einfach immer weiter in Teilaufgaben gebrochen werden können.

  • Kommutative Reduktionsoperationen (Summe / Produkt)
  • Rekursive Algorithmen (Sortierung / Mutation von Bäumen)

Reguläre Platformthreads

Schwergewichtige oder langlebige (lowlevel) Tasks.

  • GUI-Thread
  • Dedizierte Hintergrundüberwachung
  • Worker, der direkt mit Hardware interagiert

Unterschiedliche Threadstates

Ein Thread kann verschiedene Arten der “Inaktivität” haben. Das hier sind die Unterschiede:

Status Thread.State Erklärung
Ungestartet NEW Der Thread wurde erstellt, aber #start() wurde noch nicht aufgerufen. #isAlive() gibt false zurück.
Unterbrochen
(Interrupted)
Beliebig Ein Thread wurde mit #interrupt dazu aufgefordert, seine Aufgabe so schnell es geht abzuschließen. #isInterrupted() gibt true zurück.
IO-Blockiert
(Parked)
RUNNABLE Der Thread wartet auf ein Signal vom OS, kann jedoch, in der Zwischenzeit, in der JVM, weiterlaufen. Dieses Blockieren in einer Methode kann mit #interrupt() sowohl sofort und präemptiv unterbrochen werden und führt zu einer InterruptedException
Gesperrt BLOCKED Ein Thread ist in einen gesperrten synchronized-Block gelaufen oder hängt an einem Lock. Der Thread ist in der JVM blockiert und kann weder Arbeit annehmen, noch die Blockierung unterbrechen.
Wartet WAITING Der aktuelle Thread blockiert, nachdem Object#wait darin aufgerufen wurde. Dieses Warten kann entweder durch eine Unterbrechung oder den Aufruf von notifyAll() auf dem jeweiligen Objekt beendet werden.
Schläft TIMED_WAITING Der Thread wird für eine bestimmte Zeit blockiert und nimmt danach seine Arbeit wieder auf. Er kann, während er schläft, unterbrochen werden.
Beendet TERMINATED Die Methode #run() im Thread ist beendet. Die Aufgabe ist abgeschlossen und #isAlive() gibt false zurück. Ein beendeter Thread kann nicht erneut gestartet werden.

FAQ

Ein paar häufig gestellte Fragen aus dem Internet; unter anderem von StackOverflow.

Frage Antwort
Wie viele Threads soll ich im Threadpool benutzen? Probier es aus. Miss die Performance und passe die Ober- und Untergrenzen immer wieder an. Bei CPU-Tasks sind die Anzahl der verfügbaren Kerne ein sinnvoller Startpunkt.
Platform Threads oder Virtual Threads? Platform Threads für Berechnungen, Virtual Threads für blockierende IO-Aufgaben.
Wie viele Virtual Threads kann ich gleichzeitig starten? Praktisch so viele wie Speicher da ist. Zehntausende bis Millionen sind üblich, weil sie kaum Ressourcen brauchen, solange sie blockieren.
Sollte ich meinen Stream parallelisieren? Wahrscheinlich nicht. Aber miss die Zeiten bei einer durchschnittlichen Arbeitslast mit und ohne.
Sollte ich Multithreading einsetzen um die Performance zu steigern? Überlege dir gut, wie lang die Aufgaben sequentiell brauchen. Wenn du es nur für ein paar Millisekunden machst, ist es vermutlich nicht die Komplexität wert.
Wie gehe ich mit einer InterruptedException um? Im Mainthread: Vermutlich ein Bug. Im Nebenthread: Schließe alle Verbindungen, gebe alle Ressourcen frei, brich deine aktuelle Transaktion ab, logge, und beende so schnell wie möglich. Wenn das nicht sofort geht, setze das Flag erneut mit (Thread.currentThread().interrupt().
Sollte ich Thread extenden? Auch wenn man Thread#run() überschreiben kann, solltest du lieber eine Implementierung von Runnable and den Konstruktor übergeben.
Was war noch gleich ein Daemon Thread? Ein Thread, welcher dein Programm nicht am schließen hindert. (Hintergrundtask)
Was ist der Unterschied zwischen Runnable und Callable Ein Runnable hat keinen Rückgabewert und kann nur unchecked Exceptions werfen. Callable hat einen beliebigen Rückgabewert und kann checked Exceptions werfen.
Woher weiß ich, ob der aktuelle Thread der Mainthread ist? "main".equals(Thread.currentThread().getName())
Wie kann ich einen Thread stoppen / killen? Gar nicht! Thread#stop() wurde in Java 1.2 deprecated, da es extrem unsicher ist. Nutze stattdessen Thread#interrupt().
Was passiert, wenn ich join() aufrufe? Der aufrufende Thread blockiert, bis der andere fertig ist, kann aber dabei unterbrochen werden.