CodeGym /Kurse /JAVA 25 SELF /Scoped Values und neue Thread-Mechanismen (Java 21+)

Scoped Values und neue Thread-Mechanismen (Java 21+)

JAVA 25 SELF
Level 57 , Lektion 4
Verfügbar

1. Warum ThreadLocal an Relevanz verliert

Wozu braucht man ThreadLocal überhaupt?

In der klassischen Nebenläufigkeit, in der Threads lange leben (zum Beispiel auf dem Server), muss man bisweilen threadeigene Daten speichern – solche, die sich nicht mit anderen kreuzen dürfen. Etwa Benutzername, Request-ID oder ein temporärer Puffer.

Dazu gibt es in Java ThreadLocal<T> – so etwas wie ein „persönlicher Bereich“ eines Threads, in dem man Daten ablegen kann, ohne die Nachbarn zu stören:

ThreadLocal<String> user = new ThreadLocal<>();

user.set("Alice"); // der Wert gilt nur für diesen Thread
String name = user.get(); // liefert hier „Alice“, in anderen Threads – null

Warum ThreadLocal mit virtuellen Threads nicht harmoniert

Virtuelle Threads leben ganz anders als die alten „schweren“ Threads. Sie entstehen und verschwinden zu Tausenden – mitunter in Millisekundenbruchteilen. Und ThreadLocal bindet Daten an einen konkreten Thread, als würde er ewig leben.

Wenn ein virtueller Thread seine Arbeit beendet, können seine Daten in ThreadLocal im Speicher hängenbleiben – selbst wenn der Thread längst beendet ist. Das führt zu Lecks, denn die JVM weiß nicht immer, dass diese Werte niemand mehr braucht.

Werden Threads wiederverwendet (zum Beispiel in Pools), ist eine noch unangenehmere Situation möglich: „Fremder“ Kontext kann versehentlich an eine neue Anfrage weitergereicht werden. Stellen Sie sich vor, Benutzer Peter erhält die Daten von Rutger – hallo Bugs und Sicherheitslücken.

ThreadLocal fühlt sich dort wohl, wo es wenige Threads gibt und sie lange leben. Mit virtuellen Threads ist das hingegen so, als würde man Dinge in einem Schrank lagern, der jede Sekunde verschwindet.

2. Scoped Values: eine neue Art, Kontext zu übergeben

Scoped Values sind ein neues Werkzeug aus Java 21, das das alte Problem von ThreadLocal elegant löst. Statt Daten im Thread zu speichern, wie bei ThreadLocal, „heftet“ es sie an den Ausführungsbereich – also an einen konkreten Codeabschnitt. Der Wert lebt nur, solange dieser Abschnitt ausgeführt wird, und verschwindet danach automatisch, ohne Speicherreste zu hinterlassen.

import java.lang.ScopedValue;

ScopedValue<String> USER = ScopedValue.newInstance();

ScopedValue.where(USER, "Alice").run(() -> {
    System.out.println("Hello, " + USER.get()); // Gibt aus: Hello, Alice
});

Sobald der Code den run-Block verlässt, ist der Wert nicht mehr verfügbar – ein Zugriff darauf löst eine Ausnahme aus. Man muss nichts von Hand aufräumen.

Scoped Values vermüllen den Speicher nicht, verwechseln keinen Kontext zwischen Threads und erlauben verschachtelte Bereiche, in denen innere Werte äußere vorübergehend überlagern. Das ist ein sauberer, vorhersehbarer und sicherer Weg, Kontext zu übergeben – insbesondere in der Welt virtueller Threads.

3. Beispiele für den Einsatz von Scoped Values

Beispiel 1: Benutzerkontext übergeben

Angenommen, wir haben einen Server, der Anfragen verschiedener Benutzer verarbeitet. Für jede Anfrage möchten wir wissen, wer sie initiiert hat.

import java.lang.ScopedValue;

public class ServerExample {
    static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) {
        processRequest("Alice");
        processRequest("Bob");
    }

    static void processRequest(String userName) {
        ScopedValue.where(USER, userName).run(() -> {
            handleBusinessLogic();
        });
    }

    static void handleBusinessLogic() {
        System.out.println("Wir verarbeiten für Benutzer: " + USER.get());
    }
}

Was passiert:

  • Für jede Anfrage wird ein eigener Scope erstellt, in dem USER auf „Alice“ bzw. „Bob“ gesetzt ist.
  • Innerhalb von handleBusinessLogic() erhalten wir stets den korrekten Benutzernamen.
  • Sobald die Verarbeitung der Anfrage beendet ist, verschwindet der Wert.

Beispiel 2: Logging mit Kontext

Angenommen, wir möchten die Request-ID automatisch in Logs einfügen:

import java.lang.ScopedValue;

public class LoggingExample {
    static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            String reqId = "REQ-" + i;
            ScopedValue.where(REQUEST_ID, reqId).run(() -> {
                log("Beginn der Verarbeitung");
                doWork();
                log("Ende der Verarbeitung");
            });
        }
    }

    static void log(String message) {
        System.out.printf("[%s] %s%n", REQUEST_ID.get(), message);
    }

    static void doWork() {
        log("Wir arbeiten ...");
    }
}

Ergebnis (Beispiel):

[REQ-1] Beginn der Verarbeitung
[REQ-1] Wir arbeiten ...
[REQ-1] Ende der Verarbeitung
[REQ-2] Beginn der Verarbeitung
[REQ-2] Wir arbeiten ...
[REQ-2] Ende der Verarbeitung
[REQ-3] Beginn der Verarbeitung
[REQ-3] Wir arbeiten ...
[REQ-3] Ende der Verarbeitung

Jeder Scope hält seine eigene Request-ID; eine Verwechslung zwischen Threads ist unmöglich.

4. Scoped Values und virtuelle Threads: das ideale Paar

Warum Scoped Values mit virtuellen Threads besonders nützlich sind

Virtuelle Threads leben nur kurz – sie werden zu Tausenden erstellt und wieder beendet, mitunter in Bruchteilen einer Sekunde. Daher funktioniert der alte Ansatz mit ThreadLocal, bei dem Daten fest an den Thread gebunden sind, hier schlicht nicht: Threads verschwinden zu schnell, und der Kontext kann versehentlich leaken oder verwechselt werden.

ScopedValue bindet Daten hingegen an die Aufgabe selbst – an ihren Ausführungsbereich. Das bedeutet: Der Kontext (zum Beispiel Benutzername oder Request-ID) folgt dem Code, nicht dem Thread. Wenn die Aufgabe endet, verschwindet der Wert automatisch. Für virtuelle Threads ist das die ideale Lösung: sicher, sauber und ohne Überraschungen.

Beispiel: Massenverarbeitung mit virtuellen Threads

import java.lang.ScopedValue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadScopedValueDemo {
    static final ScopedValue<Integer> TASK_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 10_000; i++) {
            int taskId = i;
            executor.submit(() -> ScopedValue.where(TASK_ID, taskId).run(() -> {
                processTask();
            }));
        }

        executor.shutdown();
    }

    static void processTask() {
        // Für jede Aufgabe eine eigene TASK_ID
        System.out.println("Verarbeiten Aufgabe #" + TASK_ID.get());
    }
}

Schlüsselstellen:

  • Für jede Aufgabe entsteht ein eigener Scope für TASK_ID.
  • Selbst wenn Aufgaben parallel laufen, werden Werte nicht zwischen Threads vermischt.
  • Keine Speicherlecks: Der Scope „stirbt“ zusammen mit der Aufgabe.

5. Vergleich: ThreadLocal vs ScopedValue

Kriterium ThreadLocal ScopedValue
Bindung An den Thread An den Codebereich (Scope)
Lebenszyklus Solange der Thread lebt Solange der Scope ausgeführt wird
Sicherheit Risiko von Leaks, Verwechslungen Keine Leaks, keine Verwechslungen
Virtuelle Threads Ineffizient, riskant Ideal geeignet
Verwendung
set/get
where(...).run(...), get
Verschachtelung Unterstützt kein Override Werte können überlagert werden

6. Verschachtelte Bereiche (Scopes): Werte überlagern

ScopedValue<String> INFO = ScopedValue.newInstance();

ScopedValue.where(INFO, "Äußerer").run(() -> {
    System.out.println(INFO.get()); // "Äußerer"
    ScopedValue.where(INFO, "Innerer").run(() -> {
        System.out.println(INFO.get()); // "Innerer"
    });
    System.out.println(INFO.get()); // "Äußerer"
});

Ergebnis:

Äußerer
Innerer
Äußerer

Das ist praktisch, wenn innerhalb einer Aufgabe der Kontextwert vorübergehend überschrieben werden soll.

Scoped Values: typische Anwendungsfälle

  • Übertragung von Benutzer- oder Request-ID: um Aktionen zu loggen oder Berechtigungen zu prüfen.
  • Logging: automatische Kontext-Einfügung in Logs.
  • Tracing: für Debugging und Profiling.
  • Transaktionsparameter: etwa Isolationsstufe oder Betriebsmodus.
  • Beliebiger „Kontext“, der nur innerhalb einer Aufgabe (oder ihrer Unteraufgaben) sichtbar ist.

7. Weitere neue Mechanismen: Structured Concurrency

Structured Concurrency ist ein Ansatz, bei dem zusammengehörige Aufgaben (zum Beispiel Teilprozesse einer Operation) als Einheit verwaltet werden: Wenn die übergeordnete Aufgabe beendet ist oder fehlschlägt, werden alle untergeordneten Aufgaben automatisch abgebrochen. Das verringert das Risiko „vergessener“ oder „hängender“ Threads.

Beispiel (stark vereinfacht):

try (var scope = StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> result1 = scope.fork(() -> fetchData1());
    Future<String> result2 = scope.fork(() -> fetchData2());

    scope.join(); // auf Abschluss beider warten
    scope.throwIfFailed(); // wenn mindestens eine fehlgeschlagen ist – Ausnahme werfen

    String combined = result1.resultNow() + result2.resultNow();
    System.out.println(combined);
}

Vorteile:

  • Sauberere Verwaltung des Lebenszyklus von Aufgaben.
  • Keine „hängenden“ Teilprozesse.
  • Fehler lassen sich leichter behandeln.

Structured Concurrency befindet sich derzeit im Vorschau-Modus, wird aber bereits aktiv weiterentwickelt.

8. Praktische Hinweise und Einschränkungen

Wann sollte man Scoped Values einsetzen?

  • Immer dann, wenn Kontext zwischen Aufgaben übertragen werden soll – insbesondere mit virtuellen Threads.
  • Wenn Sie bisher ThreadLocal verwendet haben, überlegen Sie, ob ein Wechsel zu ScopedValue sinnvoll ist.

Wann braucht man ThreadLocal weiterhin?

  • In seltenen Fällen, wenn ein Thread sehr lange lebt und der Kontext für seine gesamte Lebensdauer „konstant“ sein muss (zum Beispiel bei der Arbeit mit Legacy-Code).

Einschränkungen

  • Scoped Values lassen sich nach dem Erzeugen eines Scope nicht mehr ändern – sie sind read-only.
  • Scoped Values dürfen außerhalb des Scope nicht verwendet werden: Ein Zugriff außerhalb löst eine Ausnahme aus.
  • Verwenden Sie Scoped Values nicht zur Speicherung großer Objekte – der Bereich sollte leichtgewichtig und schnell sein.

9. Typische Fehler beim Einsatz von Scoped Values

Fehler Nr. 1: Versuch, den Wert außerhalb des Scope abzurufen. Wenn Sie USER.get() außerhalb des Blocks ScopedValue.where(...) aufrufen, erhalten Sie eine NoSuchElementException. Stellen Sie sicher, dass der Zugriff ausschließlich innerhalb des Bereichs erfolgt.

Fehler Nr. 2: Versuch, den Wert innerhalb des Scope zu ändern. Scoped Values sind kein veränderlicher Container. Wenn Sie einen Wert vorübergehend „überschreiben“ möchten, erstellen Sie einen verschachtelten Scope.

Fehler Nr. 3: ThreadLocal und ScopedValue zusammen verwenden. Mischen Sie diese Mechanismen nicht ohne zwingenden Grund – das kann zu Kontextverwechslungen und Fehlern führen.

Fehler Nr. 4: Logik nicht in den Block run() eingebettet. Wenn Sie ScopedValue.where(USER, "Alice") ohne .run(() -> { ... }) schreiben, wird kein Scope erzeugt!

Fehler Nr. 5: Versuch, Scoped Value für langlebige globale Informationen zu verwenden. Für solche Zwecke verwenden Sie besser normale Variablen oder ThreadLocal (sofern gerechtfertigt).

1
Umfrage/Quiz
Virtuelle Threads, Level 57, Lektion 4
Nicht verfügbar
Virtuelle Threads
Virtuelle Threads
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION