W3docs

Java Thread-Klasse

Threads in Java erstellen und steuern: Thread-Klasse erweitern oder Runnable übergeben – mit den jeweiligen Vor- und Nachteilen.

java.lang.Thread ist das Objekt, das du in der Hand hältst, wenn du einen Ausführungs-Thread starten, benennen, joinen, unterbrechen oder abfragen möchtest. Das vorherige Kapitel hat Threads auf konzeptioneller Ebene eingeführt; dieses hier ist die API-Tour. Alles in java.util.concurrent — Executors, Futures, virtuelle Threads — baut auf Thread auf, daher lohnt es sich, die rohe Klasse zu kennen, auch wenn du im Produktionscode meistens zu den übergeordneten Abstraktionen greifst.

Zwei Wege, einen Thread zu erstellen

Ein Thread ist ein Runnable, das in ein Steuerungsobjekt eingewickelt ist. Es gibt zwei Möglichkeiten, ihm das Runnable zu übergeben:

// 1. Pass a Runnable to the constructor (the modern, preferred form)
Thread a = new Thread(() -> System.out.println("hello from " + Thread.currentThread().getName()));

// 2. Extend Thread and override run()
class HelloThread extends Thread {
  @Override public void run() {
    System.out.println("hello from " + getName());
  }
}
Thread b = new HelloThread();

Beide Varianten funktionieren; beide führen deinen Code auf einem neuen Thread aus. Die erste Form ist es, die praktisch der gesamte moderne Code verwendet — aus drei Gründen:

  • Eine Klasse kann nur eine andere Klasse erweitern. Wenn du Thread erweiterst, kannst du nichts anderes mehr erweitern — und der Teil deines Codes, der die Arbeit ist, hat selten einen guten Grund, im OO-Sinne ein Thread zu sein. Ein Runnable zu übergeben, hält deine Business-Klasse frei.
  • Lambdas machen die Runnable-Form zu einem Einzeiler. Das Unterklassieren von Thread erfordert eine benannte Klasse für denselben Code.
  • Das Runnable, das du übergibst, kann später auch an einen ExecutorService weitergegeben werden. Die Thread-Unterklasse ist darauf festgelegt, auf ihrem eigenen dedizierten Thread zu laufen.

Erweitere Thread nur, wenn du wirklich Zustand oder Methoden zum Thread selbst hinzufügen möchtest (selten). Für alles andere übergibst du ein Runnable.

Starten und Warten

Die zwei Methoden, die du bei fast jedem Thread verwenden wirst:

Thread t = new Thread(() -> doWork(), "worker");
t.start();                                    // schedule it; return immediately
t.join();                                     // block the caller until the thread finishes

Einige typische Anfängerfehler hierbei:

  • start() ist das, was den OS-Thread erstellt. run() direkt aufzurufen führt den Rumpf auf dem aktuellen Thread aus, synchron — kein neuer Thread wird gestartet. Das ist der mit Abstand häufigste Multithreading-Fehler für Einsteiger. Wenn du kein start() siehst, hat keine Parallelverarbeitung stattgefunden.
  • start() kann nur einmal aufgerufen werden. Ein Thread ist einmalig verwendbar. start() ein zweites Mal aufzurufen wirft IllegalThreadStateException. Um dieselbe Aufgabe erneut auszuführen, erstelle einen neuen Thread oder verwende einen ExecutorService.
  • join() kann InterruptedException werfen. Es ist ein blockierender Aufruf. Wenn jemand interrupt() auf dem Thread aufruft, der in join() wartet, endet das Warten mit der Exception. Du musst sie behandeln oder weitergeben.

join(millis) wartet höchstens so viele Millisekunden, bevor es zurückkehrt, unabhängig davon, ob der Thread fertig ist oder nicht. Verwende es, wenn du einem Worker eine begrenzte Chance geben möchtest, graceful zu beenden, bevor du eskalierst.

Die wichtigen Konstruktoren

Thread hat viele Konstruktoren; in der Praxis sind vier relevant:

KonstruktorWann zu verwenden
new Thread(Runnable)Der Basisfall. Anonymer Worker.
new Thread(Runnable, String name)Fast immer vorzuziehen — Namen erscheinen in Logs, Profilern, Thread-Dumps.
new Thread(ThreadGroup, Runnable, String)Wenn du eine explizite Gruppe benötigst (selten; Gruppen sind weitgehend veraltet).
new Thread(ThreadGroup, Runnable, String, long stackSize)Wenn der Standard-Stack (etwa 1 MB) falsch ist — z. B. bei tiefer Rekursion oder Speicherdruck.

Der leere new Thread()-Konstruktor existiert und führt ein leeres run() aus, das nichts tut. Es gibt keinen Grund, ihn zu verwenden.

Benenne deine Threads immer. "worker-1", "http-3", "flush-loop" — was auch immer die Rolle ist. Ein Thread-Dump voller Thread-7, Thread-12, Thread-19 ist ein Thread-Dump, den du nicht lesen kannst.

Eigenschaften einer Thread-Instanz

Die Felder und Getter, die du tatsächlich anfassen wirst:

t.setName("scanner-2");                       // any time before or after start()
String name = t.getName();

t.setDaemon(true);                            // BEFORE start(); else IllegalThreadStateException
boolean d = t.isDaemon();

t.setPriority(Thread.NORM_PRIORITY);          // 1..10; mostly advisory, see chapter 6
int p = t.getPriority();

Thread.State s = t.getState();                // NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
boolean alive = t.isAlive();                  // true between start() and run() returning
long id = t.threadId();                       // Java 19+; old name: getId()

Zwei davon sind am wichtigsten:

  • setDaemon(true) entscheidet, ob der Thread die JVM am Leben hält. Siehe das vorherige Kapitel — Daemon-Threads sterben mit dem Programm; Nicht-Daemon-Threads halten es am Laufen, bis sie zurückkehren.
  • getState() ist das, was du in einem Thread-Dump anschaust, um zu diagnostizieren, warum ein Thread feststeckt. BLOCKED bedeutet, er wartet auf ein intrinsisches Lock; WAITING/TIMED_WAITING bedeutet, er ist in wait(), join(), sleep(), LockSupport.park() usw. geparkt.

Statische Hilfsmethoden auf Thread

Einige statische Methoden, die du aus dem Worker heraus aufrufen wirst:

Thread.currentThread();                       // the thread that's executing this code
Thread.sleep(2000);                           // pause this thread for ~2000 ms
Thread.yield();                               // hint to the scheduler "go ahead and run someone else"
Thread.interrupted();                         // returns and CLEARS the interrupt flag of currentThread

Thread.sleep ist die häufigste; sie wirft InterruptedException, sodass Aufrufer sie behandeln oder weitergeben müssen. Thread.yield ist fast nie das richtige Werkzeug — es ist ein vager Hinweis, den die JVM und das Betriebssystem ignorieren können. Wenn du koordinieren möchtest, verwende ein echtes Synchronisationsprimitiv.

Thread.interrupted() gibt true zurück, wenn der aktuelle Thread unterbrochen wurde, und löscht das Flag. t.isInterrupted() (Instanzmethode, auf einem anderen Thread) gibt das Flag zurück, ohne es zu löschen. Sie zu verwechseln ist eine häufige Quelle steckengebliebener Interrupts.

Unterbrechung: Wie man einen Thread zum Stoppen auffordert

Es gibt kein sicheres t.stop() (die Methode existiert, ist aber seit 1.1 veraltet, weil sie Locks gehalten und den Zustand korrumpiert hinterlässt). Das kooperative Shutdown-Protokoll lautet:

Thread worker = new Thread(() -> {
  while (!Thread.currentThread().isInterrupted()) {
    doOneUnitOfWork();
  }
}, "worker");
worker.start();
// ... later, from somewhere else:
worker.interrupt();
worker.join();

interrupt() setzt das Interrupt-Flag des Workers. Der Worker soll das Flag an sicheren Punkten prüfen und beenden. Wenn der Worker in sleep, wait, join oder vielen java.nio-Aufrufen blockiert ist, wirft der blockierende Aufruf sofort InterruptedException, damit der Thread reagieren kann.

Wenn du InterruptedException abfängst und sie nicht weitergeben möchtest, ist die Konvention, das Flag neu zu setzen, damit Aufrufer weiter oben im Stack den Interrupt noch sehen:

try { Thread.sleep(1000); }
catch (InterruptedException e) {
  Thread.currentThread().interrupt();         // re-arm the flag
  return;                                     // and give up cooperatively
}

Einen Interrupt zu schlucken, ohne das Flag neu zu setzen, ist ein Bug. Das Flag ist der Weg, wie der Rest des Programms weiß, dass du aufgefordert wurdest zu stoppen.

Ein ausgearbeitetes Beispiel: Der vollständige Lebenszyklus in einem Programm

Das folgende Programm erstellt zwei Worker auf verschiedene Arten (Runnable, Unterklasse), beobachtet ihre Zustandsübergänge, joined sie und demonstriert das Interrupt-Protokoll an einem dritten Worker.

java— editable, runs on the server

Was man aus dem Lauf mitnehmen kann:

  • Die Zustandsübergänge entsprechen dem Vertrag. Vor start() waren beide Threads NEW. Nach start() waren sie RUNNABLE (oder TERMINATED, wenn die Arbeit winzig war und vor dem Print fertig wurde). Nach join() waren beide TERMINATED. Das ist der Lebenszyklus, den Thread.State beschreibt.
  • Die Zeile "t3 ran on thread: main" ist der Bug, den man sich für immer merken sollte. t3.run() hat den Rumpf ausgeführt — auf dem aufrufenden Thread, synchron. Es wurde kein neuer Thread erstellt. t3.isAlive() war danach false, weil start() nie aufgerufen wurde. Wenn du debuggst, warum "nichts parallel zu laufen scheint", prüfe, ob du start() oder run() geschrieben hast.
  • Die Interrupt-Schleife verwendete Thread.sleep nicht als Hauptwartemechanismus — sie prüfte nur fleißig das Flag, mit einem gelegentlichen kurzen Sleep, damit der Interrupt den Sleep früh beenden konnte. Der Vertrag ist auf beide Arten gleich: isInterrupted() ist das, was der Worker abfragt; interrupt() ist das, was der Anforderer aufruft.
  • Das Neu-Setzen des Flags im catch-Block (die Thread.currentThread().interrupt()-Zeile) hat das Signal für jeden Code weiter oben im Aufruf-Stack erhalten. Ohne diese Zeile würde ein abgefangener und ignorierter Interrupt verschwinden — was eine der einfachsten Methoden ist, einen Thread zu schreiben, der nicht sauber herunterfährt.
  • Der Daemon am Ende war dabei, 60 Sekunden zu schlafen; stattdessen beendete die JVM, sobald main zurückkehrte, und tötete ihn mitten im Sleep. Daemon-Threads können beliebige Ressourcen halten — aber sie können auch jederzeit unterbrochen werden, weshalb du keine commit-pflichtige Arbeit auf ihnen ablegen solltest.

Was kommt als Nächstes

Das nächste Kapitel, Java Runnable Interface, befasst sich eingehend mit Runnable selbst — was es wirklich ist, warum Callable und Future darüber hinzugefügt wurden, und wie Lambdas die Ergonomie des Übergebens von Arbeit an einen Thread verändert haben.

Übungen

Übung
Du rufst `t.run()` (nicht `t.start()`) auf einem `Thread` auf, dessen `Runnable` den Namen des aktuellen Threads ausgibt. Was wird ausgegeben?
Du rufst `t.run()` (nicht `t.start()`) auf einem `Thread` auf, dessen `Runnable` den Namen des aktuellen Threads ausgibt. Was wird ausgegeben?
Was this page helpful?