CodeGym /Kurse /JAVA 25 SELF /Starten von Threads: Thread und Runnable, Syntax

Starten von Threads: Thread und Runnable, Syntax

JAVA 25 SELF
Level 51 , Lektion 1
Verfügbar

1. Die Klasse Thread: der erste Thread in Java

In Java wird jeder Ausführungsthread durch ein Objekt der Klasse Thread repräsentiert. Diese Klasse ist wie ein Dirigent im Orchester – sie steuert Start, Stopp und den Lebenszyklus des Threads.

Um einen Thread zu starten, brauchst du:

  1. Ein Objekt der Klasse Thread (oder einer Unterklasse) zu erzeugen.
  2. Bei diesem Objekt die Methode start() aufzurufen.

Schauen wir uns das in der Praxis an.

Beispiel 1. Von Thread erben

Der direkteste Weg, einen Thread zu erstellen, besteht darin, eine eigene Klasse von Thread abzuleiten und ihre Methode run() zu überschreiben. Alles, was du innerhalb von run() schreibst, wird in einem eigenen Thread ausgeführt.

// Einfacher Thread über Vererbung
public class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hallo aus dem Thread! Ich heiße: " + getName());
    }
}

public class Main {
    public static void main(String[] args) {
        HelloThread thread = new HelloThread(); // wir erzeugen ein Thread-Objekt
        thread.start(); // wir starten den Thread
        System.out.println("Der Haupt-Thread beendet seine Arbeit.");
    }
}

Was passiert eigentlich?
Wenn du thread.start() aufrufst, erstellt Java einen neuen Thread und startet darin die Methode run(). Der Haupt-Thread (derjenige, der mit main begonnen hat) wartet dabei nicht, sondern arbeitet parallel weiter.

Und die wichtigste Warnung: Verwechsle start() nicht mit einem direkten Aufruf von run(). Wenn du thread.run() schreibst, wird kein neuer Thread erzeugt – das ist nur ein normaler Methodenaufruf, der im selben Thread ausgeführt wird, in dem du ihn aufrufst. Echte Nebenläufigkeit beginnt erst mit start().

Was ist getName()?

Die Methode getName() gibt den Namen des Threads zurück. Standardmäßig vergibt Java Namen wie "Thread-0", "Thread-1" usw. Das ist praktisch zum Debuggen.

2. Das Interface Runnable: die beste Methode für die meisten Fälle

Java ist eine Sprache, die Flexibilität liebt. Die Klasse Thread erbt bereits von Object, und in Java gibt es keine Mehrfachvererbung von Klassen. Wenn du deine Klasse von Thread ableitest, kannst du sie nicht gleichzeitig noch von einer anderen Klasse erben lassen (wenn du z. B. eine Klasse Car hast und daraus einen Thread machen möchtest – du musst dich entscheiden).

Die Lösung: Die Thread-Logik in ein separates Objekt auslagern, das das Interface Runnable implementiert. Dieses Interface hat genau eine Methode run(). Anschließend erzeugst du ein Objekt der Klasse Thread, übergibst ihm dein Runnable – und startest den Thread.

Beispiel 2. Klasse mit Runnable

// Klasse, die Runnable implementiert
public class HelloRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Hallo aus Runnable! Thread: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Runnable runnable = new HelloRunnable();
        Thread thread = new Thread(runnable); // wir übergeben das Runnable an Thread
        thread.start();
        System.out.println("Der Haupt-Thread beendet seine Arbeit.");
    }
}

Hier erstellen wir die Klasse HelloRunnable, die das Interface Runnable implementiert. In der Methode main wird ein Objekt dieser Klasse erzeugt und an den Konstruktor von Thread übergeben. Danach wird der Thread mittels start() gestartet.

Erwähnenswert ist die Methode Thread.currentThread(). Das ist eine statische Methode, mit der du das Objekt des aktuellen Threads erhältst und damit erkennen kannst, wo genau dein Code gerade läuft.

Warum ist das besser?

  • Deine Klasse kann von jeder anderen Klasse erben (und nicht nur von Thread).
  • Du kannst dasselbe Runnable zum Start in mehreren Threads wiederverwenden.
  • Der Code wird flexibler und sauberer.

3. Syntax: anonyme Klassen und Lambda-Ausdrücke

Java bleibt nicht stehen! Seit Java 8 können wir noch kürzer und bequemer schreiben.

Beispiel 3. Anonyme Klasse

Manchmal möchtest du keine eigene Datei für eine Klasse anlegen, die du nur einmal brauchst. Du kannst sie direkt an der Verwendungsstelle deklarieren – das nennt man eine anonyme Klasse.

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymes Runnable! Thread: " + Thread.currentThread().getName());
            }
        });
        thread.start();
        System.out.println("Der Haupt-Thread beendet seine Arbeit.");
    }
}

Beispiel 4. Lambda-Ausdruck (Java 8+)

Das Interface Runnable ist funktional (genau eine abstrakte Methode). Daher kann man einen Lambda-Ausdruck verwenden:

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Lambda-Thread! Thread: " + Thread.currentThread().getName());
        });
        thread.start();
        System.out.println("Der Haupt-Thread beendet seine Arbeit.");
    }
}

Kurz:
Runnable wird „an Ort und Stelle“ implementiert, und der Lambda-Ausdruck liefert die Implementierung der Methode run().

4. Mehrere Threads starten

Du kannst beliebig viele Threads starten (nun, fast – es hängt von den Ressourcen des Rechners ab).

Beispiel 5. Wir starten mehrere Threads

public class Main {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            int threadNumber = i; // für die Lambda unbedingt eine Kopie der Variable erstellen!
            Thread thread = new Thread(() -> {
                System.out.println("Thread #" + threadNumber + ": hallo!");
            });
            thread.start();
        }
        System.out.println("Der Haupt-Thread beendet seine Arbeit.");
    }
}

Die Ausgabe kann unterschiedlich sein!
Threads laufen parallel – welcher seine Nachricht zuerst ausgibt, hängt vom Betriebssystem und dem Thread-Scheduler ab.

5. Lebenszyklus eines Threads

Zu verstehen, wie ein Thread „lebt“, ist wichtig für das Debugging und den korrekten Einsatz von Threads.

Wesentliche Zustände eines Threads

  • NEW – der Thread wurde erzeugt, aber noch nicht gestartet (new Thread(...)).
  • RUNNABLE – der Thread wurde gestartet (start()), kann ausgeführt werden.
  • TERMINATED – der Thread hat die Ausführung beendet (die Methode run() ist zu Ende).

Was passiert bei start() und run()

  • start() – erstellt einen neuen Thread und ruft dessen Methode run() in diesem neuen Thread auf.
  • run() – eine gewöhnliche Methode; wenn sie direkt aufgerufen wird, läuft sie im aktuellen Thread (sie erzeugt keinen neuen Thread!).

Beispiel:

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("run() aus dem Thread: " + Thread.currentThread().getName()));
        thread.run();   // WIRD IM HAUPT-THREAD AUFGERUFEN!
        thread.start(); // WIRD IN EINEM SEPARATEN THREAD AUFGERUFEN!
    }
}

6. Praxis: wir bauen eine einfache Multithreading-Anwendung

Machen wir weiter mit unserer Übungsanwendung – nehmen wir an, wir schreiben ein einfaches Loggingsystem, in dem jeder Thread seine eigene Nachricht schreibt.

public class LoggerTask implements Runnable {
    private final String message;

    public LoggerTask(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        System.out.println("[" + Thread.currentThread().getName() + "] Log: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Thread logger1 = new Thread(new LoggerTask("Systemstart"));
        Thread logger2 = new Thread(new LoggerTask("Daten werden geladen"));
        Thread logger3 = new Thread(new LoggerTask("Anfrage wird verarbeitet"));

        logger1.start();
        logger2.start();
        logger3.start();

        System.out.println("Der Haupt-Thread beendet seine Arbeit.");
    }
}

Was passiert?
Drei Threads schreiben parallel ihre Nachrichten auf die Konsole. Die Reihenfolge der Nachrichten ist nicht garantiert – das ist genau das Wesen der Nebenläufigkeit.

7. Nützliche Feinheiten

Tabelle: Vergleich der Möglichkeiten, Threads zu erstellen

Methode Flexibilität Wiederverwendbarkeit Empfohlen?
Von Thread erben Niedrig Nein Nur für einfache/Lehrbeispiele
Runnable implementieren Hoch Ja Ja
Anonyme Klasse Mittel Nein Für einmalige Aufgaben
Lambda-Ausdruck Hoch Nein Ja (Java 8+)

Schema: wie der Start eines Threads funktioniert

+----------------------+ 
|  main (Haupt-Thread) |
+----------------------+
           |
           v
+----------------------+
|  Thread thread = ... |
+----------------------+
           |
           v
+----------------------+
|  thread.start()      |  ← Der Thread "erwacht"
+----------------------+
           |
           v
+----------------------+
|  run() im neuen Thread|
+----------------------+
           |
           v
+----------------------+
|  Thread beendet      |
+----------------------+

8. Typische Fehler beim Starten von Threads

Fehler Nr. 1: run() statt start() aufrufen.
Ein sehr häufiger Anfängerfehler ist es, die Methode run() eines Thread-Objekts direkt aufzurufen. In diesem Fall wird kein neuer Thread erzeugt; die Methode wird einfach im aktuellen (Haupt-)Thread ausgeführt. Richtig: immer start() zum Starten eines Threads verwenden.

Fehler Nr. 2: Thread erneut starten.
Du kannst start() für dasselbe Objekt Thread nicht mehr als einmal aufrufen – das führt zu IllegalThreadStateException. Wenn du die Aufgabe erneut starten musst, erstelle ein neues Objekt Thread.

Fehler Nr. 3: Gemeinsame Variablen ohne Synchronisation ändern.
Wenn mehrere Threads auf dieselben Variablen zugreifen, kann das zu unerwarteten Ergebnissen führen (Race Condition). Über Synchronisation sprechen wir später, aber bis dahin – sei vorsichtig.

Fehler Nr. 4: Das Beenden von Threads nicht berücksichtigt.
Wenn es wichtig ist, auf das Ende eines Threads zu warten, nutze die Methode join(). Andernfalls kann der Haupt-Thread früher enden als die Kind-Threads.

Fehler Nr. 5: Thread-Name und Klassenname verwechseln.
Die Methode getName() gibt den Namen des Threads zurück, nicht den Namen der Klasse. Für das Debugging ist es oft hilfreich, Thread-Namen explizit über den Konstruktor oder setName() zu setzen.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION