ThreadLocal: Doğru İdiom, Yanlış Çağ
Java EE Servlet konteynerleri, 1999: bir talepte bir thread. Bir thread baştan sona bir talebi işler ve sonra sona erer. ThreadLocal şu anda çalıştırılan thread'e göre bir değer tutar. Birden fazla talepte bir thread, ThreadLocal'da bir değer, tam olarak bir talebe aittir. İdiom: doğru.
Thread havuzları sözleşmeği değiştirdi. Bir thread talebi A'yı işler, ThreadLocal'da principal A'yi kaydederek talebi A'yi bitirir ve havuz'a geri döner. Thread havuzları thread durumu resetlenmez. ThreadLocal.remove() temizlik yapar, ancak bu, açıkça disiplin gerektirir. Disiplinin başarısız olması durumunda, talebi B çalıştırır ve aynı thread'de principal A'yı ThreadLocal'da okur.
The 5-step leak:
1. Talep A ulaşır. Server Thread-7'yi atar.
2. Thread-7 ThreadLocal.set(principal_A) talebin başlangıcındaayarlar.
3. Talep A tamamlanır. Thread-7 havuz'a geri döner. ThreadLocal.remove() çağrılmez.
4. Talep B ulaşır. Server Thread-7'yi (havuz yeniden kullanımı) atar.
5. Thread-7 ThreadLocal.get() okur: principal_A geri döner. Talep B yanlış kimlikle çalışır.
Neden Testler Katılamaz
Birim testleri izole çalıştırır: thread havuzu yok, yeniden kullanım yok. Entegrasyon testleri yeni threadler kullanır veya testler arasında durumu sıfırlar. Yük testleri doğru kullanıcılar ve düşük paralellikle düzgün bir şekilde ısınır. Sızıntı, thread havuzu yeniden kullanımı ve geçen taleplerle ortaya çıkan bir durumdaysa, bu durumu kontrol eden herhangi bir test konfigürasyonu yoktur.
Güvenlik Sonucu
Kullanıcı A'nın kimliği kullanıcı B'nin talebine sızar. Bir kaza değil. Bir istisna değil. Sessiz bir güvenlik sınırı ihlalidir: kullanıcı B, kullanıcı A'nın verilerini okur veya kullanıcı B'nin izinlerini atlar. Sistem hata üretmez. Loglar, talep B'nin yetkilendirildiğini gösterir. Her şey düzgün görünür.
Beş Adım
ThreadLocal sızıntısı, yanlış kod çalıştırıldığında değil, temizleme adımlarının eksik olduğu anda meydana gelir.
Birimle Bağlı Değerler
ThreadLocal, bir değeri bir thread'a bağlar. Bir thread, bir talep sonrası yaşar. Uyuşmazlık.
Birimle bağlı değerler, bir iş birimiyle bir değer bağlar. İş birimi bittiğinde, değer de biter. Açıkça temizleme yok. remove() ile unutmak yok.
Java 21: ScopedValue
// ThreadLocal (Eksik taşıyıcı)
static final ThreadLocal<Principal> PRINCIPAL = new ThreadLocal<>();
PRINCIPAL.set(principal); // talep başlangıcında ayarlanır
// ... talep işleme ...
PRINCIPAL.remove(); // çağrılması gerekir; genellikle unutulur
// ScopedValue (DOĞRU taşıyıcı)
static final ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
ScopedValue.where(PRINCIPAL, principal).run(() -> {
// ... talep işleme ...
// değer, run() döndüğünde otomatik olarak gitmiş olur
});
Go: context.Context
// context.Context, değerleri açıkça taşır; zamanlama = fonksiyon çağrı zinciri
ctx := context.WithValue(r.Context(), principalKey, principal)
handleRequest(ctx) // ctx açıkça geçirilir; fonksiyondan döndüğünde kaybolur
Python asyncio: contextvars.ContextVar
# ContextVar, her async görev için özel
PRINCIPAL: ContextVar[str] = ContextVar('principal')
token = PRINCIPAL.set(principal) # bu görev için ayarlanır
# ... görev işleme ...
PRINCIPAL.reset(token) # veya: görev sonlandığında
Bu değerleri paylaşan özelliğin: iş birimiyle eşleşen ömür. İstek sona erdiğinde (run() döner, fonksiyon döner, görev tamamlanır), değer sona erer. Temizleme unutmak için. Hiçbir havuz kirlenmeyecek.
Bul & Yerine Geçir
Java EE uygulaması, istek başlangıcında kiracının ID'sini ThreadLocal'da depolar. Yüksek yükleşitme sırasında kiracı A'nın ID'si, kiracı B'nin isteklerinden görünür. Kiracı B'nin sorguları, kiracı A'nın verilerini geri getirir. Hiçbir istisna atılmaz. Kusar sadece üretim yük testi sırasında.