Nebenläufigkeit bedeutet, dass ein Programm mehrere Aufgaben überlappend bearbeiten kann. Das heißt nicht automatisch, dass wirklich alles exakt gleichzeitig passiert. Es bedeutet zuerst einmal nur: Dein Programm wartet nicht stur darauf, dass eine Aufgabe vollständig fertig ist, bevor es mit der nächsten weitermacht.
Ein einfaches Beispiel ist eine Java-Anwendung, die Daten aus einer Datenbank lädt, eine Datei schreibt und gleichzeitig auf neue Eingaben reagieren soll. Würde alles streng nacheinander laufen, könnte sich die Anwendung schnell träge anfühlen. Während eine Aufgabe wartet, zum Beispiel auf die Datenbank oder auf eine Antwort aus dem Netzwerk, kann eine andere Aufgabe bereits weiterlaufen.
Dazu gibt es in Java Threads. Ein Thread ist ein eigener Ausführungspfad innerhalb deines Programms. Du kannst dir das grob wie mehrere Hände vorstellen, die am selben Arbeitsplatz Aufgaben erledigen. Das klingt praktisch, ist es auch, aber nur solange diese Hände sich nicht gegenseitig in die Quere kommen.
Wichtig ist der Unterschied zwischen Nebenläufigkeit und Parallelität. Nebenläufigkeit beschreibt, dass mehrere Aufgaben zeitlich ineinander verschachtelt ablaufen können. Parallelität bedeutet, dass Aufgaben wirklich zur gleichen Zeit auf mehreren CPU-Kernen laufen. Ein Programm kann nebenläufig sein, auch wenn auf der Hardware gerade nicht alles wirklich parallel ausgeführt wird. Das Betriebssystem und die JVM wechseln dann sehr schnell zwischen den Threads hin und her.
Für dich ist anfangs vor allem diese Erkenntnis wichtig: Sobald mehrere Abläufe denselben Code oder dieselben Daten verwenden, reicht es nicht mehr, nur von oben nach unten zu denken. Dein Code kann plötzlich in einer Reihenfolge ausgeführt werden, die du beim Schreiben nicht direkt vor Augen hast.
Warum Nebenläufigkeit in Java schnell relevant wird
Du musst nicht selbst Threads starten, damit Nebenläufigkeit in deinem Alltag auftaucht. In vielen Java-Anwendungen ist sie schon da, bevor du bewusst darüber nachdenkst. Eine Webanwendung auf WildFly verarbeitet zum Beispiel mehrere HTTP-Requests gleichzeitig. Wenn zwei Benutzer zur selben Zeit dieselbe Seite aufrufen oder dieselbe REST-Schnittstelle verwenden, laufen diese Aufrufe nicht sauber hintereinander wie in einem kleinen Konsolenprogramm.
Das ist ein großer Unterschied zu den ersten Übungen, bei denen du vielleicht eine main-Methode startest und der Code Schritt für Schritt abläuft. In einer Server-Anwendung gibt es oft viele gleichzeitige Zugriffe. Der Application Server kümmert sich darum, dass Requests angenommen, Threads verwaltet und Antworten zurückgeschickt werden. Das nimmt dir Arbeit ab, aber es nimmt dir nicht die Verantwortung für sauberen Code.
Gerade deshalb solltest du bei gemeinsam genutzten Objekten vorsichtig sein. Ein Feld in einer Klasse wirkt harmlos, kann aber problematisch werden, wenn dieselbe Instanz von mehreren Threads genutzt wird. In JavaEE- oder Jakarta-EE-Anwendungen ist das besonders relevant, weil Services, Beans oder Ressourcen je nach Scope und Konfiguration länger leben können als ein einzelner Methodenaufruf.
So ein Code sieht auf den ersten Blick unspektakulär aus:
public class ZaehlerService {
private int anzahl = 0;
public void erhoehen() {
anzahl++;
}
public int getAnzahl() {
return anzahl;
}
}
In einem einzelnen Thread funktioniert das scheinbar sauber. Der Wert wird erhöht und anschließend wieder gelesen. Sobald aber zwei Threads gleichzeitig erhoehen() aufrufen, wird es interessant. anzahl++ ist nämlich nicht eine einzige unteilbare Aktion. Java liest den aktuellen Wert, erhöht ihn und schreibt ihn zurück. Wenn zwei Threads denselben alten Wert lesen, kann eine Erhöhung verloren gehen.
Genau solche Fehler machen Nebenläufigkeit unangenehm. Der Code sieht korrekt aus, kompiliert ohne Probleme und läuft vielleicht hundertmal richtig. Beim nächsten Start fehlt plötzlich ein Wert. Danach funktioniert wieder alles. Solche Fehler sind schwer zu finden, weil sie vom Timing abhängen.
Das eigentliche Problem ist gemeinsamer Zustand
Nebenläufigkeit wird meistens dann gefährlich, wenn mehrere Threads denselben veränderbaren Zustand verwenden. Zustand bedeutet hier: Daten, die sich während der Laufzeit ändern. Das kann ein Zähler sein, eine Liste, eine Map, ein Cache, ein Warenkorb oder ein Feld in einem Service.
Wenn Daten nur gelesen werden, ist die Situation meist einfach. Wenn mehrere Threads dieselbe unveränderliche Konfiguration lesen, passiert nichts Kritisches. Problematisch wird es, wenn mindestens ein Thread schreibt und andere Threads gleichzeitig lesen oder ebenfalls schreiben.
Nimm eine normale ArrayList. Sie ist praktisch, aber nicht thread-sicher. Wenn ein Thread gerade ein Element hinzufügt und ein anderer Thread gleichzeitig über dieselbe Liste iteriert, kannst du kaputte Zustände, unerwartete Ergebnisse oder Exceptions bekommen. Die Liste schützt dich nicht automatisch davor.
Ein besserer erster Gedanke ist deshalb nicht: Welche Synchronisierung brauche ich? Der bessere erste Gedanke ist: Muss dieser Zustand überhaupt geteilt werden?
Sehr oft ist die sauberste Lösung, Daten lokal zu halten. Eine lokale Variable innerhalb einer Methode gehört nur zu diesem Methodenaufruf. Wenn jeder Request seine eigenen lokalen Daten verarbeitet, gibt es deutlich weniger Risiko.
public BigDecimal berechneSumme(List<BigDecimal> preise) {
BigDecimal summe = BigDecimal.ZERO;
for (BigDecimal preis : preise) {
summe = summe.add(preis);
}
return summe;
}
summe ist hier lokal. Jeder Aufruf bekommt seine eigene Variable. Auch wenn mehrere Threads diese Methode gleichzeitig verwenden, teilen sie sich nicht dieselbe summe. Das ist gut. Genau solche einfachen Strukturen sind in nebenläufigem Code oft robuster als eine clevere Lösung mit globalem Zustand.
Wenn du wirklich gemeinsamen Zustand brauchst, musst du ihn bewusst schützen. Für einfache Zähler gibt es zum Beispiel AtomicInteger:
public class ZaehlerService {
private final AtomicInteger anzahl = new AtomicInteger();
public void erhoehen() {
anzahl.incrementAndGet();
}
public int getAnzahl() {
return anzahl.get();
}
}
Das ist nicht automatisch die Lösung für jedes Problem, aber für einfache atomare Zähler ist es deutlich sauberer als ein ungeschütztes int. Für andere Fälle gibt es synchronized, Locks oder thread-sichere Collections wie ConcurrentHashMap. Diese Werkzeuge solltest du aber nicht blind einsetzen. Sie lösen bestimmte Probleme und bringen neue Fragen mit, zum Beispiel zu Performance, Lesbarkeit und möglichen Deadlocks.
Ein Deadlock entsteht, wenn Threads gegenseitig aufeinander warten und keiner mehr weiterkommt. Das passiert nicht in jedem kleinen Programm, aber es zeigt gut, warum Nebenläufigkeit Respekt verdient. Du reparierst nicht einfach nur Syntax. Du modellierst Abläufe, die gleichzeitig passieren können.
Fazit: Begrenze Nebenläufigkeit bewusst
Nebenläufigkeit ist kein Spezialthema, das nur große Systeme betrifft. Sobald mehrere Benutzer, mehrere Requests, Hintergrundaufgaben oder parallele Berechnungen ins Spiel kommen, bist du mittendrin. Java gibt dir dafür viele Werkzeuge, aber Werkzeuge ersetzen kein klares Denken.
Der wichtigste Punkt ist: Teile veränderbare Daten nicht leichtfertig. Halte Variablen so lokal wie möglich. Verwende unveränderliche Objekte, wenn es passt. Achte in Webanwendungen darauf, welche Objekte pro Request entstehen und welche länger leben. Ein Feld in einer Klasse ist nicht automatisch privat im Sinne von "nur für diesen einen Benutzer". Es ist nur privat im Sinne von Sichtbarkeit innerhalb der Klasse.
Starte auch nicht einfach eigene Threads in einer Server-Anwendung, nur weil es technisch möglich ist. In einer JavaEE- oder Jakarta-EE-Umgebung verwaltet der Server die Threads. Wenn du Hintergrundarbeit brauchst, solltest du die dafür vorgesehenen Mechanismen der Plattform verwenden. So bleibt deine Anwendung kontrollierbarer und passt besser in die Laufzeitumgebung.
Für den Anfang reicht ein ruhiger Grundsatz: Nebenläufigkeit ist nicht gefährlich, weil sie kompliziert klingt. Sie ist gefährlich, weil Fehler selten offensichtlich sind. Ein falscher Zugriff auf gemeinsame Daten kann lange unbemerkt bleiben und erst unter Last sichtbar werden.
Wenn du nebenläufigen Code liest oder schreibst, frag dich immer: Welche Daten werden geteilt? Wer darf sie ändern? Kann ein anderer Thread denselben Zustand zur gleichen Zeit sehen? Gibt es eine klare Grenze zwischen lokalem Zustand und gemeinsamem Zustand?
Diese Fragen wirken unspektakulär, aber sie sparen dir später viele Stunden Fehlersuche. Sauberer nebenläufiger Code entsteht nicht dadurch, dass du überall synchronized ergänzt. Er entsteht dadurch, dass du möglichst wenig teilen musst und das Wenige, was geteilt wird, bewusst absicherst.
