ThreadLocal ve ScopedValue — Havuzdaki Thread Neyi Hatırlıyor?
Önce şunu oku: Virtual Threads vs Platform Threads , Lambda ve Fonksiyonel Arayüzler — Lambda Neyi Yakalar?
30 saniyede özet
ThreadLocal her thread'e özel bir çekmece verir. Havuzdaki thread ise istekten isteğe geçer ve çekmeceyi kendisi boşaltmaz; remove() unutulursa bir kullanıcının bilgisi ötekine kalır.
Bir kullanıcı sipariş listesini açıyor ve başka birinin siparişlerini görüyor. Kendi bilgisayarında denediğinde olmuyor, yalnızca site kalabalıkken oluyor.
-
Bayt: Bir kullanıcı başka birinin siparişlerini görmüş!
-
Sen: Kendi bilgisayarımda denedim, olmuyor.
-
Bayt: Yalnızca site kalabalıkken oluyorsa şüpheli belli: havuzdaki thread'ler.
-
Bayt: Thread ölmüyor, istekten isteğe geçiyor. Peki çekmecesini kim boşaltıyor?
ThreadLocal: thread başına bir çekmece
ThreadLocalAynı değişken için her thread'e ayrı bir değer veren yapı. Değer remove() edilene ya da thread ölene kadar kalır; havuzdaki thread ise ölmez.Sözlükte gör →, aynı değişken için her thread’e ayrı bir değer verir. Bir istek boyunca “şu anki kullanıcıyı” her metoda parametre olarak geçmeden taşımanın en eski yoludur.
Farkında olmadan her gün kullanırsın: SLF4J’nin MDC’si, Spring’in SecurityContextHolder’ı ve transaction senkronizasyonu varsayılan olarak ThreadLocal’dır.
Kafam karıştı, daha basit anlat
ThreadLocal, her thread’e ait kişisel bir çekmecedir. Aynı isimli çekmece, her thread için ayrı bir şey saklar.
`static final ThreadLocal<User> CURRENT` alanı `static` olduğu hâlde iki thread aynı anda farklı kullanıcıları nasıl tutabiliyor?
Havuzda thread ölmez
alice'in isteği W1'de CURRENT.set(alice) ile işlendi, remove() yok. Sonra anonim bir istek W1'e düştü ve set çağrılmadı. CURRENT.get() ne döner? Cevabı göster
alice. Havuzdaki thread isteği bitirince ölmez; bir sonraki isteğe geçer. Çekmecede ne bıraktıysan, sonraki istek onu bulur.
Adım adım oku
- alice'in isteği havuzdaki W1 thread'ine düşer ve çekmeceye, yani ThreadLocal'a kendi adını koyar.
- İstek biter, ama remove() çağrılmadığı için çekmece dolu kalır. Thread ölmez, havuza döner.
- Sıradaki anonim istek aynı thread'e düşer ve set çağırmadan çekmeceye bakar: alice'i bulur.
- Çözüm: finally içinde remove() çağırmak ya da blok bitince kendiliğinden sona eren ScopedValue kullanmak.
Sunucular istekleri bir thread poolGörevleri sabit ya da sınırlı sayıda, yeniden kullanılan thread'e dağıtan yapı. Her görev için yeni thread açmanın bellek ve zamanlama maliyetini önler.Sözlükte gör → ile işler. Değer, sen remove() edene kadar thread’de kalır. Kural tek satırdır: set ettiğin yerde, finally içinde temizle.
CURRENT.set(user);try { chain.doFilter(request, response);} finally { CURRENT.remove();}Kafam karıştı, daha basit anlat
Havuzdaki thread iş bitince eve gitmez, sıradaki işe geçer. Çekmeceyi boşaltmadıysan, sonraki iş eskisinin eşyalarını bulur. Bu yüzden finally içinde temizle.
Yük altında anonim bir istek başka bir kullanıcının verisini görüyor. Kullanıcı bir filtrede ThreadLocal'a set ediliyor, remove() yok. Kök neden ne?
Çoğu zaman doğru çalışan bu filtre, arada bir isteği yanlış tenant'la işliyor. Hatalı satır hangisi?
Kendin gör
Havuzdaki thread neyi hatırlıyor?
Tohum 353793static final ThreadLocal<User> CURRENT = new ThreadLocal<>(); void handle(Request req) { if (req.user() != null) CURRENT.set(req.user()); orders.list(CURRENT.get()); // null = anonim}W1 (istek havuzu)
boş
W2 (istek havuzu)
boş
A1 (audit executor)
boş
Oynat ya da adımla.
Şu an ne oldu?
Üç istek, iki havuz thread'i
alice W1'de, bob W2'de, sonra anonim bir istek yine W1'de. W1 alice'i hâlâ hatırlıyor mu?
Görevler0/3
Anonim bir isteğin başka birinin kimliğiyle çalışmasını sağlaaçık
İpucu
Havuz thread'i önceki isteği unutmasın.
Bir kullanıcının audit kaydından düşmesine yol açaçık
İpucu
Bağlamı başka bir executor'da ara.
Audit açıkken ne sızıntı ne kayıp olsunaçık
İpucu
Thread sonunda temizlensin; executor'a değer olarak geçsin.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
- Varsayılanla oynat. Anonim istek alice’in siparişlerini gördü.
- Temizliği “finally” yap. Sızıntı bitti.
- Audit’i “Executor’da CURRENT oku” yap. alice ve bob audit kaydından düştü.
- Bağlamı ScopedValue yap. Sızıntı yok, ama audit sorunu aynı.
- Audit’i “Önce oku, kopyala” yap. Temiz.
Simülatörde audit görevi executor'da `CURRENT` okuyunca alice ve bob kayıttan düştü; ScopedValue'da da. Neden?
ScopedValue: süresi olan bağ
ScopedValueBir değeri yalnızca belirli bir kod bloğu çalışırken görünür kılan, değiştirilemez bağ. Blok bitince bağ da biter. Java 25'te kalıcı oldu.Sözlükte gör → Java 25’te kalıcı oldu; 21–24 arasında preview’dı. Değer bir kod bloğuna bağlanır ve blok bitince bağ da biter.
ScopedValue.where(CURRENT, user).run(() -> orders.list());Temizlenecek bir şey yoktur ve değer blok içinde değiştirilemez. Bağın dışında get() çağırmak NoSuchElementException fırlatır.
StructuredTaskScope ile açılan alt görevler bağı devralır; o API Java 25’te hâlâ preview. Sıradan bir executor’a verilen iş ise devralmaz.
Kafam karıştı, daha basit anlat
ScopedValue, belli bir kod bloğu boyunca geçerli bir etikettir. Blok bitince etiket kendiliğinden kalkar, temizlemeyi unutmak diye bir şey olmaz.
`ScopedValue.where(CURRENT, alice).run(task)` çağrısı bittikten sonra aynı thread'de `CURRENT.get()` çağrılırsa ne olur?
ScopedValue hangi Java sürümünde preview'dan çıkıp kalıcı oldu?
Tuzaklar
@Async ve supplyAsync. İş başka bir thread’de çalışır; SecurityContextHolder ve MDC orada boştur. Bağlamı kopyalayan bir TaskDecorator kullan.
InheritableThreadLocal ve havuz. Değer thread oluşturulurken kopyalanır; havuzun thread’i ise bir kez, rastgele bir istek sırasında oluşur.
Virtual thread’e ağır ThreadLocal. Milyonlarca virtual thread, milyonlarca kopya demektir. Thread başına cache yerine paylaşılan, thread-safe bir nesne kullan.
Bir `@Async` metodunda `SecurityContextHolder.getContext().getAuthentication()` null dönüyor; controller'da doluydu. En uygun çözüm hangisi?
Aşağıdaki örnek bir bankanın mobil API’sinden: müşteri numarası isteğin bağlamında üç yolla taşınıyor. finally ile temizlenen ThreadLocal, başka bir havuza geçerken bağlamı kopyalayan executor ve kapsamı blokla sınırlı ScopedValue. Son dosya sızıntıyı gözle görülür kılıyor.
Derinleş · Mobil bankacılıkta müşteri bağlamı: ThreadLocal, kopyalama ve ScopedValue 5 dosya · ~81 satır · ilk okumada atlayabilirsin
Kendini sına
ThreadLocal, aynı değişken için her thread'e ayrı bir değer verir.
Bir takım, 'alt thread'ler de bağlamı görsün' diye ThreadLocal'ı InheritableThreadLocal'a çeviriyor. İşler sabit boyutlu bir thread havuzunda çalışıyor. Ne olur?
Aklında kalacak üç şey
- 1 Havuzdaki thread bir isteği bitirince ölmez, sonrakine geçer. ThreadLocal'a koyduğun değer, remove() edilene kadar orada kalır.
- 2 set ile remove() arası try/finally ile korunur; yoksa ilk hata bir sonraki isteğe başkasının kimliğini bırakır.
- 3 Ne ThreadLocal ne ScopedValue başka bir thread havuzuna kendiliğinden geçer. Thread değiştiren işe bilgiyi değer olarak ver.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.