Wenn du in Java zum ersten Mal über Konkurrenz und Threads stolperst, wirkt das Thema schnell größer, als es eigentlich sein muss. Das liegt vor allem daran, dass mehrere Begriffe durcheinandergeworfen werden. Mal ist von Parallelität die Rede, mal von Nebenläufigkeit, mal einfach nur von Threads. Dazu kommen dann noch Begriffe wie Synchronisierung, Race Condition oder Deadlock. Für den Einstieg reicht es aber, wenn du die Grundlagen sauber auseinanderhalten kannst. Genau darum geht es hier.

 

Was Konkurrenz und Threads in Java eigentlich bedeuten

Im Java Umfeld ist mit Konkurrenz meistens Nebenläufigkeit gemeint. Gemeint ist damit, dass mehrere Aufgaben in einem Programm zeitlich überlappend bearbeitet werden. Das heißt noch nicht automatisch, dass sie wirklich gleichzeitig auf verschiedenen CPU Kernen laufen. Genau an der Stelle ist die Unterscheidung wichtig.

Nebenläufigkeit bedeutet, dass mehrere Abläufe unabhängig voneinander organisiert sind. Parallelität bedeutet, dass sie tatsächlich zur gleichen Zeit ausgeführt werden. Ein Programm kann also nebenläufig aufgebaut sein, auch wenn auf dem Rechner gerade nur ein CPU Kern aktiv für diese Aufgabe arbeitet. Das Betriebssystem und die Laufzeitumgebung wechseln dann sehr schnell zwischen den Abläufen hin und her.

Ein Thread ist dabei ein Ausführungspfad innerhalb eines Prozesses. Ein Java Programm startet zunächst mit einem Hauptthread, dem main Thread. Wenn du weitere Threads erzeugst, kann dein Programm mehrere Aufgaben gleichzeitig verwalten. Diese Threads teilen sich dabei den Speicher des Prozesses. Genau das ist praktisch, weil sie gemeinsam auf dieselben Objekte zugreifen können. Genau das ist aber auch die Quelle vieler Fehler.

Ein kleines Beispiel macht den Unterschied greifbar:

public class Main {
    public static void main(String[] args) {
        Runnable task = () -> System.out.println("Läuft in einem eigenen Thread");

        Thread thread = new Thread(task);
        thread.start();

        System.out.println("Läuft im main Thread");
    }
}

Wichtig ist hier der Aufruf von start(). Nur dadurch wird wirklich ein neuer Thread gestartet. Wenn du stattdessen run() direkt aufrufst, läuft der Code ganz normal im aktuellen Thread weiter. Das ist ein typischer Anfängerfehler.

Du kannst dir einen Thread also als eigenen Arbeitskontext vorstellen. Jeder Thread hat seinen eigenen Aufrufstack, aber mehrere Threads können auf dieselben Objekte im Heap zugreifen. Deshalb musst du bei gemeinsam genutzten Daten aufpassen.

 

Wofür du Threads überhaupt brauchst

Threads sind sinnvoll, wenn dein Programm mehrere Dinge tun soll, ohne dass sich diese Aufgaben gegenseitig unnötig blockieren. Ein klassischer Fall ist eine Benutzeroberfläche. Wenn dort ein langsamer Datenbankzugriff oder ein Netzwerkanruf im gleichen Thread läuft wie die Oberfläche, friert die Anwendung schnell sichtbar ein. Mit einem separaten Thread kann die Oberfläche weiter reagieren, während im Hintergrund gearbeitet wird.

Auch bei Serveranwendungen spielen Threads eine große Rolle. Wenn mehrere Anfragen gleichzeitig eintreffen, sollen sie nicht nacheinander abgearbeitet werden, sondern parallel oder zumindest nebenläufig. Genau deshalb nutzen Webserver, Application Server und Frameworks intern Thread Pools.

Für dich als Entwickler ist der wichtigere Punkt aber oft ein anderer: Du musst verstehen, wann ein Problem nebenläufig organisiert werden sollte und wann nicht. Nur weil eine Aufgabe etwas länger dauert, brauchst du nicht automatisch eigene Threads. Gerade am Anfang ist die sauberere Lösung oft, erst einmal sequentiellen Code zu schreiben und nur dann Nebenläufigkeit einzubauen, wenn du wirklich einen klaren Nutzen hast.

Wenn du in Java eine Aufgabe asynchron ausführen willst, ist ein Executor meistens sinnvoller als new Thread(...) an vielen Stellen im Code:

ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> datenLaden());
executor.shutdown();

Der Unterschied ist wichtig. Mit Thread startest du direkt einen einzelnen Ausführungspfad. Mit einem ExecutorService delegierst du Aufgaben an einen verwalteten Pool. Das ist in echten Anwendungen oft die bessere Richtung, weil du nicht unkontrolliert immer mehr Threads erzeugst.

 

Wo die Probleme anfangen und warum du vorsichtig sein solltest

Sobald mehrere Threads auf dieselben Daten zugreifen, wird es spannend. Dann geht es nicht mehr nur darum, dass etwas gleichzeitig läuft, sondern darum, ob die Ergebnisse zuverlässig bleiben. Genau hier entstehen viele klassische Fehler.

Ein einfaches Beispiel ist ein Zähler:

class Counter {
    private int value = 0;

    void increment() {
        value++;
    }

    int getValue() {
        return value;
    }
}

Auf den ersten Blick sieht das harmlos aus. Wenn aber zwei Threads gleichzeitig increment() aufrufen, ist das Ergebnis nicht mehr zuverlässig. value++ ist nämlich keine einzelne unteilbare Operation. Intern wird gelesen, erhöht und wieder geschrieben. Wenn sich zwei Threads da in die Quere kommen, gehen Erhöhungen verloren.

Das nennt man eine Race Condition. Mehrere Threads konkurrieren um denselben Zustand, und das Ergebnis hängt vom Timing ab. Genau deshalb sind Nebenläufigkeitsfehler oft schwer zu finden. Der Code kann zehnmal funktionieren und beim elften Mal plötzlich ein falsches Ergebnis liefern.

Eine mögliche Absicherung wäre zum Beispiel AtomicInteger:

class Counter {
    private final AtomicInteger value = new AtomicInteger();

    void increment() {
        value.incrementAndGet();
    }

    int getValue() {
        return value.get();
    }
}

Für den Einstieg musst du nicht alle Synchronisierungsmechanismen im Detail kennen. Wichtiger ist, dass du das Grundproblem erkennst: Geteilter veränderbarer Zustand ist in nebenläufigem Code riskant.

Noch ein Punkt, den viele unterschätzen: Threads kosten Ressourcen. Sie brauchen Speicher, sie müssen geplant werden, und zu viele Threads können ein System sogar langsamer machen. Mehr Threads bedeuten also nicht automatisch mehr Leistung.

Dazu kommt ein praktischer Rat, gerade im Backend. In Jakarta EE oder auf einem Application Server wie WildFly solltest du Threads in der Regel nicht selbst mit new Thread(...) starten. Dort verwaltet der Container Lebenszyklus, Security Kontext und Ressourcen. Eigene Threads können diese Regeln umgehen und später schwer nachvollziehbare Probleme erzeugen. In solchen Umgebungen nutzt du besser die Mechanismen, die dir die Plattform vorgibt, zum Beispiel einen verwalteten Executor.

Wenn du noch am Anfang bist, ist deshalb eine einfache Regel hilfreich: Nutze Threads nicht, nur weil es technisch möglich ist. Nutze sie, wenn ein konkretes Problem gelöst werden muss, zum Beispiel blockierende Arbeit im Hintergrund oder klar getrennte unabhängige Aufgaben. Sobald gemeinsam veränderbare Daten ins Spiel kommen, solltest du besonders aufmerksam werden.

 

Fazit

Konkurrenz in Java bedeutet, dass mehrere Abläufe in einem Programm nebeneinander organisiert werden. Threads sind das technische Mittel, mit dem diese Abläufe ausgeführt werden. Das ist nützlich, wenn Arbeit im Hintergrund laufen soll, wenn eine Oberfläche reaktionsfähig bleiben muss oder wenn ein Server mehrere Aufgaben gleichzeitig bedienen soll.

Gleichzeitig ist das Thema nichts, was du leichtfertig einsetzen solltest. Nebenläufigkeit macht Code schwerer zu verstehen, schwerer zu testen und anfälliger für Fehler, die nicht konstant auftreten. Für den Einstieg reicht deshalb ein solides Grundverständnis vollkommen aus. Du solltest wissen, was ein Thread ist, warum start() nicht dasselbe ist wie run(), warum geteilter Zustand gefährlich werden kann und warum Frameworks oder Container oft die bessere Wahl sind als selbst erzeugte Threads.

Wenn du das sauber verstanden hast, bist du für die ersten Berührungspunkte mit Concurrency in Java schon deutlich besser aufgestellt als viele andere am Anfang.