CodeGym /Kurse /JAVA 25 SELF /Best Practices der Parallelprogrammierung

Best Practices der Parallelprogrammierung

JAVA 25 SELF
Level 54, Lektion 4
Verfügbar

1. Nicht alles, was sich parallelisieren lässt, sollte parallelisiert werden

In Java gibt es viele Möglichkeiten, Aufgaben zu parallelisieren. Aber „Parallelismus = immer schneller“ – das ist so, als würde man denken, dass eine Suppe mit noch mehr Salz automatisch besser schmeckt: bis zu einem gewissen Punkt – ja, darüber hinaus – lieber nicht ausprobieren.

ExecutorService eignet sich hervorragend, wenn Sie explizite Aufgaben haben, die gestartet und kontrolliert werden müssen: Request-Verarbeitung, asynchrones Laden von Daten, unabhängige Berechnungen. Sie entscheiden selbst, wie viele Threads im Pool sind, und steuern den Lebenszyklus der Aufgaben.

parallelStream ist ein schneller Weg, die Verarbeitung von Collections zu parallelisieren, wenn die Operationen unabhängig und ohne Seiteneffekte sind. Lohnt sich für „schwere“ Collections (Zehntausende von Elementen und mehr).

ForkJoinPool ist die Wahl für Aufgaben, die sich gut in Unteraufgaben aufteilen lassen (Divide & Conquer): Sortieren, Suchen, Aggregation großer Arrays. Wird innerhalb von parallelStream verwendet, kann aber auch direkt gesteuert werden.

Verwenden Sie Parallelismus nicht „für alle Fälle“. Wenn die Aufgabe klein ist, können die Overheads für Scheduling, Kontextwechsel und Synchronisation den gesamten Gewinn auffressen.

Beispiel: Wann Parallelisierung nicht nötig ist

List<Integer> smallList = List.of(1, 2, 3, 4, 5);
int sum = smallList.parallelStream()
    .mapToInt(x -> x)
    .sum(); // Für 5 Zahlen zu parallelisieren – Overkill!

2. Thread-Safety: geteilte veränderliche Daten vermeiden

In der parallelen Welt ist die Hauptgefahr – Race Conditions. Wenn mehrere Threads eine Variable ändern, kann das Ergebnis unerwartet sein.

  • Vermeiden Sie veränderliche gemeinsame Variablen. Selbst ein Ausdruck wie counter++ ist nicht atomar.
  • Verwenden Sie thread-sichere Collections und atomare Operationen. Zum Beispiel ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger, AtomicLong.
  • Keine Seiteneffekte in parallelen Streams. Ändern Sie keine äußeren Strukturen aus parallelStream.

Beispiel für schlechten Code

List<Integer> numbers = Arrays.asList(1,2,3,4,5);
List<Integer> result = new ArrayList<>();
numbers.parallelStream().forEach(n -> result.add(n * 2)); // GEFÄHRLICH!

Hier ist result.add() nicht thread-sicher. Ergebnis: verlorene Elemente oder Ausnahmen.

Wie macht man es richtig?

List<Integer> result = numbers.parallelStream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

3. Leistung: Mehr Threads ist nicht immer besser

Kleine Aufgaben zu parallelisieren lohnt sich nicht. Wenn die Arbeit Millisekunden dauert, verlangsamt paralleles Starten oft nur die Ausführung aufgrund von Overheads.

Messen Sie die Leistung. Für schnelle Messungen eignet sich System.nanoTime():

long start = System.nanoTime();
// ... Ihr Code ...
long end = System.nanoTime();
System.out.println("Ausführungszeit: " + (end - start) + " ns");

Für ernsthafte Microbenchmarks verwenden Sie JMH (Java Microbenchmark Harness).

Beispiel: Vergleich von sequentiellem und parallelem Stream

List<Integer> bigList = IntStream.range(0, 1_000_000)
    .boxed().collect(Collectors.toList());

long t1 = System.nanoTime();
long sum1 = bigList.stream().mapToLong(x -> x).sum();
long t2 = System.nanoTime();
long sum2 = bigList.parallelStream().mapToLong(x -> x).sum();
long t3 = System.nanoTime();

System.out.println("Sequenziell: " + (t2 - t1) / 1_000_000 + " ms");
System.out.println("Parallel:     " + (t3 - t2) / 1_000_000 + " ms");

Probieren Sie es auf Ihrem Rechner aus – der Nutzen ist bei wirklich großen Collections und schweren Operationen sichtbar.

4. Fehlerbehandlung: Exceptions in Threads nicht ignorieren

Future und Exception-Handling

Wenn eine Aufgabe über ExecutorService.submit() gestartet wurde, „propagieren“ Exceptions nicht automatisch – sie müssen über Future.get() behandelt werden:

Future<Integer> future = executor.submit(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Ups!");
    return 42;
});
try {
    Integer result = future.get(); // kann ExecutionException werfen
} catch (ExecutionException e) {
    System.err.println("Fehler in der Aufgabe: " + e.getCause());
}

ForkJoin und Fehlerbehandlung

Im ForkJoinPool werden Exceptions in der Aufgabe „verpackt“. Beim Aufruf von join()/get() tauchen sie auf:

ForkJoinPool pool = new ForkJoinPool();
RecursiveTask<Integer> task = new MyTask();
try {
    int result = pool.invoke(task);
} catch (Exception e) {
    System.err.println("Fehler im ForkJoin: " + e);
}

Vergessen Sie nicht die Behandlung von InterruptedException

Viele Methoden (z. B. Future.get(), Thread.sleep()) können InterruptedException werfen. „Schlucken“ Sie sie nicht – reagieren Sie korrekt: setzen Sie das Unterbrechungs-Flag oder beenden Sie die Aufgabe.

5. Debugging und Testen von parallelem Code

Logging und Debug

Parallele Fehler sind tückisch und treten oft instabil auf. Loggen Sie mit Thread-Angabe: Thread.currentThread().getName(). Das hilft zu verstehen, wer den Code wann ausführt.

In komplexen Fällen verwenden Sie einen Debugger mit Multithreading-Unterstützung (z. B. IntelliJ IDEA). Temporäre Thread.sleep() helfen manchmal, eine seltene Race Condition „einzufangen“.

Testen von Multithreading-Szenarien

Schreiben Sie separate Tests für parallele Operationen und verwenden Sie Wartetools für Bedingungen, z. B. Awaitility. Führen Sie solche Tests mehrfach aus: Manche Probleme zeigen sich erst beim 100. oder 1000. Durchlauf.

6. Nützliche Nuancen und Tipps

Lesbarkeit und Wartbarkeit: Schreiben Sie verständlichen parallelen Code

  • Dokumentieren. Kommentieren Sie komplexe Stellen und die Wahl der Werkzeuge.
  • Verwenden Sie High-Level-Abstraktionen. Bevorzugen Sie ExecutorService, parallelStream, ForkJoinPool statt manueller Thread-Steuerung.
  • Vermeiden Sie „Magie“. Verkomplizieren Sie die Synchronisation nicht unnötig, wenn es einfacher geht.

Tabelle: Welches Werkzeug wann einsetzen

Szenario Empfohlenes Werkzeug
Viele unabhängige Aufgaben
ExecutorService
Verarbeitung einer großen Collection parallelStream oder ForkJoin
Divide-and-Conquer‑Aufgabe
ForkJoinPool + RecursiveTask
Einfache asynchrone Aufgabe
CompletableFuture
Viele kleine Aufgaben Sequentieller Stream
Aufgaben mit Seiteneffekten Nur thread-sichere Collections!

„Gebote“ des Parallelprogrammierers

  • Keine gemeinsamen veränderlichen Variablen – es sei denn, Sie sind sicher, dass sie thread-sicher sind.
  • Parallelisieren Sie nicht um des Parallelisierens willen: Schätzen Sie den potenziellen Gewinn ab.
  • Vergessen Sie nicht, Pools zu schließen: shutdown()/shutdownNow().
  • Verwenden Sie parallelStream nicht für Operationen mit Seiteneffekten.
  • Vergessen Sie nicht, Exceptions aus Future und ForkJoinTask zu behandeln.
  • „Schlucken“ Sie InterruptedException nicht – beenden Sie die Aufgabe korrekt.

7. Typische Fehler bei der Parallelprogrammierung

Fehler Nr. 1: Parallelisierung kleiner Aufgaben. Anfänger parallelisieren oft alles, selbst wenn die Arbeit Mikrosekunden dauert. Ergebnis – langsamer wegen Overheads.

Fehler Nr. 2: Seiteneffekte in Streams. In parallelStream dürfen keine äußeren Variablen oder Collections verändert werden – sonst drohen Race Conditions und unvorhersehbare Bugs.

Fehler Nr. 3: Exceptions ignorieren. Wenn Fehler aus Future.get() oder ForkJoinTask nicht behandelt werden, erfahren Sie nicht, warum die Aufgabe fehlgeschlagen ist.

Fehler Nr. 4: Vergessenes shutdown() bei ExecutorService. Ohne explizites Beenden kann die Anwendung beim Beenden „hängen bleiben“.

Fehler Nr. 5: Verwendung nicht thread-sicherer Collections. Aus mehreren Threads in eine gewöhnliche ArrayList zu schreiben, ist ein direkter Weg zu Fehlern.

Fehler Nr. 6: „InterruptedException schlucken“. Wenn ein Thread unterbrochen wurde – respektieren Sie das und beenden Sie die Arbeit ordentlich.

Fehler Nr. 7: Zu komplexe Synchronisationslogik. Übermäßige synchronized-Blöcke führen zu deadlock/livelock. Bevorzugen Sie High-Level-Abstraktionen.

1
Aufgabe
JAVA 25 SELF, Level 54, Lektion 4
Gesperrt
Experiment chaotischer Magie zur Duplizierung von Artefakten 🧪
Experiment chaotischer Magie zur Duplizierung von Artefakten 🧪
1
Aufgabe
JAVA 25 SELF, Level 54, Lektion 4
Gesperrt
Analyse antiker Schriftrollen in der digitalen Bibliothek 📜
Analyse antiker Schriftrollen in der digitalen Bibliothek 📜
1
Umfrage/Quiz
Parallelismus und ForkJoin, Level 54, Lektion 4
Nicht verfügbar
Parallelismus und ForkJoin
Parallelismus und ForkJoin
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION