Platformthreads in Java (in dieser Serie fast immer direkt als “Threads” adressiert) bilden OS-Threads ab und haben zusätzlich noch ein paar Goodys, wie eigene Namen und Kontextinformationen eingebaut; diese sind jedoch eher nebensächlich.

Einen Thread kann man, als normales Objekt, über die Klasse java.lang.Thread starten und verwalten; es sind also keine Sprachkonstrukte, sondern Elemente der Standardbibliothek.

Wir erinnern uns außerdem, dass jedes Programm mindestens einen Hauptthread hat: Das ist der gewöhnliche Thread, der bisher alle unsere sequentiellen Programme ausgeführt hat. Bis auf, dass er von der JVM gestartet wird, hat er keine besonderen Eigenschaften gegenüber anderen Threads.

Der Hauptthread gibt einem anderen Thread eine Aufgabe: KI generierte Grafiken von OpenAI, August 2025

Zuerst ein paar Funktionen

Threads erstellen

Um mit Threads zu arbeiten, muss man sie erstmal erstellen. Dabei muss sofort die zu erledigende Aufgabe definiert werden, im Nachhinein kann man sie nicht mehr mitgeben. Wichtig ist, dass der Thread jedoch, nur durch das Erstellen die Aufgabe, noch nicht bearbeitet.

// Thread mit generiertem Namen
Thread thread1 = new Thread(() -> meineAufgabe());
// Thread mit beliebigem Namen (gut für übersichtliches Logging)
Thread thread2 = new Thread(() -> meineAufgabe(), "Name des Threads");

Threads starten

Wenn wir einen Thread starten wollen, können wir dies einfach machen, indem wir start() auf dem Objekt aufrufen. Die Aufgabe wird nun nebenläufig (oder parallel) erledigt und der Rest des Programmes läuft weiter. Mit isAlive() können wir testen, ob ein Thread gestartet wurde und noch nicht fertig ist.

Thread thread = new Thread(() -> meineAufgabe()); // Thread für Aufgabe erstellt
thread.start(); // Aufgabe wird bearbeitet
thread.isAlive(); // true, wenn die Aufgabe bearbeitet wird

Achtung! Einen Thread kann man nur einmal starten, auch wenn er mit seiner Aufgabe fertig ist.

Daemon-Threads

Wenn wir einen Thread zu einem (im letzten Artikel eingeführten) “Daemon Thread” machen wollen, also einen Thread, der mit Ende des Programmes terminiert, unabhängig davon ob, oder wie weit er seine Aufgabe gemacht hat, dann können wir mit setDaemon() diese Eigenschaft aktivieren. Mit isDaemon() kann man sie später für einen beliebigen Thread überprüfen.

Thread thread = new Thread(() -> meineAufgabe()); // Der neue Thread ist kein Daemon
thread.setDaemon(true); // Jetzt ist es ein Daemon-Thread
thread.isDaemon(); // true

Auf Threads warten

Wenn wir explizit warten wollen, dass ein Thread seine Aufgabe erledigt hat, zum Beispiel weil diese einen Wert erzeugt, den wir später brauchen, dann können wir mit join() den aufrufenden Hauptthread blockieren.

Thread thread = new Thread(() -> aufgabe1()); // Nebenthread für Aufgabe 1 erstellt
thread.start(); // Aufgabe 1 wird bearbeitet
aufgabe2(); // Aufgabe 2 wird vom Hauptthread bearbeitet
thread.join(); // Hauptthread wartet darauf, dass Aufgabe 1 fertig ist
thread.isAlive(); // false, die Aufgabe ist abgeschlossen

Den aktuellen Thread abfragen

Um herauszufinden, in welchem Thread der aktuelle Codeblock läuft, kann man die statische Funktion Thread.currentThread() aufrufen. Ein Beispiel hierfür ist, den Namen des aktuellen Threads auszugeben.

System.out.println("Dieser Code läuft im Thread: " + Thread.currentThread().getName());

Am Beispiel

Mit dem Gelernten können wir jetzt direkt Anfangen unser Beispielproblem zu parallelisieren. Da wir mehrere CPU-Kerne zur Verfügung haben, wissen wir, dass sich der Einsatz von mehreren Threads auf jeden Fall lohnen kann. Weil wir verschiedene Sachbearbeiter außerdem nicht unterschiedlich lange warten lassen wollen, ergibt auch die nebenläufige Ausführung von verschiedenen Aufgaben Sinn.

Wir beginnen damit, verschiedene Mappingprozesse, die nichts miteinander zutun haben, in separaten Threads auszuführen.

while (true) {
	List<SourceDataset> newestData = fetchNewestData();
	for (var sourceDataset : newestData) {
		if (!sourceDataset.isMapped()) {
			// Erstelle einen Thread, weise ihm seine Aufgabe zu und starte ihn
			new Thread(() -> mapDataset(sourceDataset)).start();
		}
	}
	TimeUnit.MINUTES.sleep(5);
}

Jetzt wäre es ja super, wenn es schon so einfach wäre. Wir haben gerade Threads etabliert und, to be fair, das Programm läuft in verschiedenen Threads und das auch auf verschiedenen CPU-Kernen. Aber es gibt noch Luft nach oben.

Probleme

  • Ein Thread ist teuer im Speicherverbrauch, da er nur durch seine Existenz schon etwa ein Megabyte an Daten bewegen kann.
  • Eine, durch Speicherverbrauch und OS-Kapazitäten, realistische Obergrenze liegt bei rund 10.000 Threads pro Maschine; auf älterer Hardware kann diese Grenze jedoch deutlich niedriger ausfallen.
  • Threads zu erstellen dauert vergleichsweise lange, da sie erst vom Betriebssystem angefordert werden müssen.
  • Je mehr Threads es gibt, desto höher ist der Scheduling-Aufwand, selbst bei Systemen mit vielen CPU-Kernen steigt der Gewinn also nicht linear.

Was bedeutet das?

Mit unserer Lösung laufen wir in mehrere Fallstricke:

  1. Wir erstellen für jeden Datensatz einen neuen Thread und werfen ihn nach getaner Arbeit weg, anstatt ihn irgendwie zu recyclen.
  2. Wenn es viele neue Datensätze gibt, oder sie lange in der Ausführung brauchen, erstellen wir genauso viele Threads und der Scheduling-Overhead steigt.
  3. Die Datei-Down- und Uploads sind blockierende IO-Operationen, wir lassen unsere Threads also recht lange warten.

In den nächsten Teilen der Serie werden wir uns Mechanismen anschauen, mit denen man diese Probleme mitigieren kann. Platformthreads, wie sie eigentlich vollständig heißen, sind ein sehr primitiver Mechanismus für Nebenläufigkeit und es gibt eine Reihe an praktischen Abstraktionen, welche die Arbeit deutlich erleichtern. Das Fazit dieses Artikels lautet also:

Fazit: Wann benutze ich Platformthreads?

Eigentlich nie direkt. Es gibt, aufgrund der genannten Eischränkungen, nur selten Gründe, Threads direkt zu instanziieren; ihre korrekte Verwaltung ist meistens einfach zu aufwändig und fehlerbehaftet.

Ein konkreter Anwendungsfall ist höchstens eine Aufgabe, die eigentlich gar nichts mit dem Rest des Programmes zutun hat, und durchgängig im Hintergrund arbeitet: zum Beispiel das Abspielen von Hintergrundmusik in einem Installationsprogramm.

Da die Entwickler von Java das ebenfalls recht schnell gemerkt haben, haben Sie bereits in Version 1.5 Executors und ExecutorServices herausgebracht. Diese sind Inhalt des nächsten Artikels.