Einführung in Parallelität und Multithreading in Java
Moderne Anwendungen erfordern zunehmend die gleichzeitige Ausführung mehrerer Aufgaben. Ob es sich um die Verarbeitung von Anfragen auf einem Server, das Herunterladen von Dateien oder komplexe Berechnungen handelt – ohne Mechanismen der Parallelität und des Multithreadings kommt man nicht aus. In Java sind diese Konzepte auf der Ebene der Sprache und der Standardbibliothek verankert, was es zu einem der besten Werkzeuge für die Erstellung leistungsfähiger Anwendungen macht.
In diesem Artikel behandeln wir alles von den Grundlagen bis zu fortgeschrittenen Werkzeugen: wie man Threads erstellt, den Zugriff auf Daten synchronisiert und moderne Hilfsprogramme aus dem Paket java.util.concurrent verwendet. Das Material wird sowohl für Anfänger als auch für Entwickler nützlich sein, die ihr Wissen systematisieren möchten.
Was ist ein Thread und wozu wird er benötigt?
Ein Thread ist ein leichter Prozess, der innerhalb einer Anwendung ausgeführt wird. Jedes Java-Programm hat mindestens einen Thread (main). Multithreading ermöglicht die gleichzeitige Ausführung mehrerer Threads, was besonders auf Mehrkernprozessoren wichtig ist.
Die wichtigsten Vorteile von Multithreading:
- Leistungssteigerung durch parallele Aufgabenausführung.
- Verbesserte Reaktionsfähigkeit der Benutzeroberfläche.
- Effiziente Nutzung von Systemressourcen.
1. Threads in Java erstellen: klassische Ansätze
In Java gibt es zwei grundlegende Möglichkeiten, einen Thread zu erstellen: durch Vererbung von der Klasse Thread und durch Implementierung des Interfaces Runnable. Betrachten wir beide.
Vererbung von der Klasse Thread
Der einfachste Weg ist, eine Unterklasse von Thread zu erstellen und die Methode run() zu überschreiben.
public class MyThread extends Thread { @Override public void run() { System.out.println("Thread " + getName() + " gestartet."); }
public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // Thread starten }}Wichtig: start() aufrufen, nicht run(). run() führt den Code im aktuellen Thread aus, während start() einen neuen erstellt.
Implementierung des Interfaces Runnable
Ein flexiblerer Ansatz ist die Implementierung von Runnable und die Übergabe an den Konstruktor von Thread.
public class Task implements Runnable { @Override public void run() { System.out.println("Aufgabe wird in Thread ausgeführt: " + Thread.currentThread().getName()); }
public static void main(String[] args) { Task task = new Task(); Thread thread = new Thread(task); thread.start(); }}Diese Methode ist vorzuziehen, da sie keine Vererbung erfordert und der Klasse erlaubt, andere Klassen zu erweitern.
2. Synchronisation und Atomarität: Wie man Datenrennen vermeidet
Wenn mehrere Threads gleichzeitig auf gemeinsame Daten zugreifen, entsteht ein Datenrennen (race condition). Um dies zu vermeiden, bietet Java Synchronisationsmechanismen.
Das Schlüsselwort synchronized
Mit synchronized kann eine Methode oder ein Codeblock für einen einzelnen Thread gesperrt werden.
public class Counter { private int count = 0;
public synchronized void increment() { count++; }
public int getCount() { return count; }}In diesem Beispiel kann nur ein Thread zu einem bestimmten Zeitpunkt increment() ausführen. Für einfache Operationen wie Inkremente sind jedoch atomare Klassen besser geeignet.
Atomare Klassen (Atomic)
Das Paket java.util.concurrent.atomic enthält Klassen, die atomare Operationen ohne Sperren ermöglichen.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter { private A