un

guest
1 / ?
back to lessons

ThreadLocal: Korrekter Spruch, Falsche Ära

Java EE Servlet-Container, um 1999: Ein Thread pro Anforderung. Ein Thread bearbeitet genau eine Anforderung von Anfang bis Ende und beendet sich dann. ThreadLocal speichert einen Wert, der an den aktuellen Thread angelehnt ist. Mit einem-Thread-pro-Anforderung gehört ein im ThreadLocal gespeicherter Wert genau einer Anforderung. Der Spruch: korrekt.

Threadpools änderten den Vertrag. Ein Thread bearbeitet Anforderung A, speichert den Principal A in ThreadLocal, beendet Anforderung A und kehrt in den Pool zurück. Threadpools resetten den Thread-Zustand nicht. ThreadLocal.remove() bereinigt, aber es erfordert explizite Disziplin. Wenn die Disziplin versäumt wird, läuft Anforderung B auf demselben Thread und liest Principal A in ThreadLocal.

Der 5-Schritt-Geleak:

1. Anforderung A tritt auf. Der Server gibt Thread-7 zu.

2. Thread-7 setzt ThreadLocal.set(principal_A) am Anforderungsbeginn.

3. Anforderung A ist abgeschlossen. Thread-7 kehrt in den Pool zurück. ThreadLocal.remove() wird nicht aufgerufen.

4. Anforderung B tritt auf. Der Server gibt Thread-7 (Pool-Nutzung) zu.

5. Thread-7 liest ThreadLocal.get(): gibt principal_A zurück. Anforderung B läuft unter der falschen Identität.

Warum Tests es verpassen

Einheitstests laufen isoliert: Kein Threadpool, keine Wiederverwendung. Integrationstests verwenden frische Threads oder resetten den Zustand zwischen Tests. Lasttests wärmen mit korrekten Benutzern und niedriger Konkurrenz auf. Die Fehlererscheinung manifestiert sich nur unter Threadpool-Nutzung mit überlappenden Anforderungen, eine Bedingung, die sich in der Produktion unter normalen Verkehr zeigt, nicht in einer Testkonfiguration, die danach sucht.

Die Sicherheitsfolge

Principal A von Benutzer A gelangt in die Anforderung von Benutzer B. Kein Crash. Keine Ausnahme. Eine stille Sicherheits-Grenzverletzung: Benutzer B führt Aktionen unter der Identität von A aus, liest Daten von A oder umgeht die Berechtigungen von B. Das System zeigt keine Fehler. Die Protokolle zeigen, dass Anforderung B autorisiert war. Alles sieht korrekt aus.

Die Fünf Schritte

Die fünf Schritte eines ThreadLocal-Geleaks sind genau: Der Fehler tritt nicht erst in dem Moment auf, in dem die falsche Code ausgeführt wird. Er tritt früher auf, im Fehlen einer Bereinigungsschritt.

Gehen Sie durch die 5 Schritte. An welchem Schritt tritt der Fehler auf und warum würde ein Test-Suite ihn verpassen?

Werte, die an den Scope gebunden sind

ThreadLocal bindet eine Werte an einen Thread. Ein Thread überlebt eine Anfrage. Missmatch.

Scope-bundene Werte binden eine Werte an eine Einheit von Arbeit. Wenn die Einheit von Arbeit endet, endet auch der Wert. Keine explizite Bereinigung. Kein remove() um den Wert zu vergessen.

Java 21: ScopedValue

// ThreadLocal (Defekt-Träger)
static final ThreadLocal<Principal> PRINCIPAL = new ThreadLocal<>();
PRINCIPAL.set(principal);           // Wert wird bei Anfrage-Start gesetzt
// ... Anfrage-Handling ...
PRINCIPAL.remove();                 // Muss aufgerufen werden; oft vergessen

// ScopedValue (RICHTIGER Träger)
static final ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
ScopedValue.where(PRINCIPAL, principal).run(() -> {
    // ... Anfrage-Handling ...
    // Wert wird automatisch entfernt, wenn run() zurückkehrt
});

Go: context.Context

// context.Context trägt Werte explizit; Scope = Funktionsaufruf-Kette
ctx := context.WithValue(r.Context(), principalKey, principal)
handleRequest(ctx)  // ctx wird explizit übergeben; verschwindet, wenn Funktion zurückkehrt

Python asyncio: contextvars.ContextVar

# ContextVar ist für jede async-Aufgabe skaliert
PRINCIPAL: ContextVar[str] = ContextVar('principal')
token = PRINCIPAL.set(principal)    # Wert wird nur für diese Aufgabe gesetzt
# ... Aufgabe bearbeiten ...
PRINCIPAL.reset(token)              # oder: Bereich endet mit Aufgabe

Die Eigenschaft, die diese teilen: Das Leben entspricht der Einheit der Arbeit. Wenn die Anfrage endet (run() kehrt zurück, die Funktion kehrt zurück, die Aufgabe abgeschlossen ist), endet auch der Wert. Keine Reinigung vergessen. Kein Pool verderben.

Identifizieren & Ersetzen

Eine Java EE-Anwendung speichert die Mieter-ID in einer ThreadLocal am Anfang der Anfrage. Bei hohem Lastfall erscheint die ID des Mieters A in Anfragen des Mieters B. Die Abfragen von Mieter B liefern Daten von Mieter A. Keine Exception wird geworfen. Das Defekt tritt nur bei Produktionslasttests auf.

Was bedeutet MOAD in diesem Zusammenhang? Welcher Anbieter hat das Defekt möglich gemacht? Was ersetzt es und welche Eigenschaft des Ersatzteils verhindert das Leck?