EMZETT.
Login

Kurz: Mehrere Aufgaben können gleichzeitig ablaufen: Das Programm lädt Daten, während die Oberfläche reagiert, oder verteilt Rechenarbeit auf mehrere Kerne.

Teil des Kurses Java

Kapitel 19 von 22 im Kurs Java. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Mehrere Aufgaben können gleichzeitig ablaufen: Das Programm lädt Daten, während die Oberfläche reagiert, oder verteilt Rechenarbeit auf mehrere Kerne.

Threads

public class Faeden {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try { Thread.sleep(100); } catch (InterruptedException e) { }
            System.out.println("t1 fertig");
        });
        Thread t2 = new Thread(() -> System.out.println("t2 fertig"));
        t1.start();
        t2.start();
        t1.join();                      // warten, bis t1 beendet ist
        t2.join();
        System.out.println("alle fertig");
    }
}

Ausgabe:

t2 fertig
t1 fertig
alle fertig

Das Problem: gemeinsame Daten

Wenn mehrere Threads dieselbe Variable ändern, kommt es zu Wettlaufsituationen (race conditions):

public class Wettlauf {
    static int zaehler = 0;
    static final Object sperre = new Object();
 
    static void erhoehe(boolean sicher) {
        for (int i = 0; i < 100_000; i++) {
            if (sicher) synchronized (sperre) { zaehler++; }
            else zaehler++;                          // nicht atomar: Updates gehen verloren
        }
    }
    static int lauf(boolean sicher) throws InterruptedException {
        zaehler = 0;
        Thread[] t = new Thread[4];
        for (int i = 0; i < t.length; i++) { t[i] = new Thread(() -> erhoehe(sicher)); t[i].start(); }
        for (Thread x : t) x.join();
        return zaehler;
    }
    public static void main(String[] args) throws InterruptedException {
        System.out.println("mit synchronized: " + lauf(true));
        System.out.println("ohne Schutz kleiner als 400000: " + (lauf(false) < 400_000));
    }
}

Ausgabe:

mit synchronized: 400000
ohne Schutz kleiner als 400000: true

Lösungen:

WerkzeugZweck
synchronizednur ein Thread gleichzeitig im Block
AtomicInteger, AtomicLongatomare Zähler ohne Sperre
ReentrantLockflexiblere Sperren
ConcurrentHashMap, CopyOnWriteArrayListthreadsichere Collections
volatileSichtbarkeit einer Variablen für alle Threads
unveränderliche Objekte (record, final)brauchen gar keine Sperre
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
 
public class Atomar {
    public static void main(String[] args) throws Exception {
        AtomicInteger z = new AtomicInteger();
        ConcurrentHashMap<String, Integer> karte = new ConcurrentHashMap<>();
        ExecutorService pool = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 4; i++) {
            pool.submit(() -> {
                for (int k = 0; k < 10_000; k++) { z.incrementAndGet(); karte.merge("x", 1, Integer::sum); }
            });
        }
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println(z.get() + " " + karte.get("x"));
    }
}

Ausgabe:

40000 40000

ExecutorService: Thread-Pools

Threads von Hand zu starten ist aufwendig. Ein Pool verwaltet eine feste Zahl von Threads und führt Aufgaben aus:

import java.util.*;
import java.util.concurrent.*;
 
public class Pool {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        List<Future<Integer>> ergebnisse = new ArrayList<>();
        for (int n = 1; n <= 5; n++) {
            final int zahl = n;
            ergebnisse.add(pool.submit(() -> {              // Callable: liefert ein Ergebnis
                Thread.sleep(50);
                return zahl * zahl;
            }));
        }
        int summe = 0;
        for (Future<Integer> f : ergebnisse) summe += f.get();     // wartet auf das Ergebnis
        System.out.println(summe);
        pool.shutdown();
    }
}

Ausgabe:

55

CompletableFuture: asynchrone Ketten

import java.util.concurrent.*;
 
public class Zukunft {
    static CompletableFuture<Integer> laden(int id) {
        return CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(50); } catch (InterruptedException e) { }
            return id * 10;
        });
    }
    public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> a = laden(1), b = laden(2);
        CompletableFuture<Integer> summe = a.thenCombine(b, Integer::sum);
        System.out.println(summe.thenApply(x -> "Summe: " + x).get());
 
        CompletableFuture<String> fehler = CompletableFuture
            .<String>supplyAsync(() -> { throw new IllegalStateException("kaputt"); })
            .exceptionally(e -> "Ersatzwert (" + e.getCause().getMessage() + ")");
        System.out.println(fehler.get());
 
        CompletableFuture.allOf(laden(1), laden(2), laden(3)).get();
        System.out.println("alle drei fertig");
    }
}

Ausgabe:

Summe: 30
Ersatzwert (kaputt)
alle drei fertig

Virtual Threads (Java 21)

Virtual Threads sind leichtgewichtige Threads, von denen man Hunderttausende starten kann. Sie machen das einfache Programmiermodell “ein Thread pro Anfrage” auch für sehr viele gleichzeitige Verbindungen möglich:

import java.util.concurrent.*;
import java.util.stream.*;
 
public class Virtuell {
    public static void main(String[] args) throws Exception {
        try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
            var aufgaben = IntStream.range(0, 1000).mapToObj(i -> pool.submit(() -> {
                Thread.sleep(10);                 // blockiert nur den virtuellen Thread
                return i;
            })).toList();
            long summe = 0;
            for (var f : aufgaben) summe += f.get();
            System.out.println(summe);
        }
    }
}

Ausgabe:

499500

Regeln für nebenläufigen Code

  1. Unveränderlich wo möglich: final, record, List.of
  2. Gemeinsamen Zustand vermeiden oder klar schützen
  3. Höhere Werkzeuge (ExecutorService, CompletableFuture, ConcurrentHashMap) statt roher Threads
  4. Threads nicht per Thread.stop() beenden: mit interrupt() und Prüfung isInterrupted()
  5. Deadlocks vermeiden: Sperren immer in derselben Reihenfolge nehmen
  6. Testen ist schwer: Nebenläufigkeit mit Werkzeugen und Belastungstests prüfen

Merke

  • Thread, Runnable, start(), join() sind die Grundlage; besser sind Pools (ExecutorService)
  • Gemeinsame Daten schützen: synchronized, Atomic*, ConcurrentHashMap
  • Future und CompletableFuture liefern Ergebnisse asynchron
  • Virtual Threads (Java 21) erlauben sehr viele gleichzeitige Aufgaben
  • Unveränderliche Objekte vermeiden Probleme von vornherein

Übungsaufgabe

Berechne die Summe von 1 bis 1.000.000 aufgeteilt auf 4 Aufgaben in einem Thread-Pool und füge die Teilergebnisse zusammen.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Datum und Zeit

Weiter: Tests, Pakete und Projektaufbau

Alle Kapitel: Java im Überblick