İçeriğe geç

ThreadLocal ve ScopedValue — Havuzdaki Thread Neyi Hatırlıyor?

İleri 9 dk Sık karşılaşılır

Ö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.

  1. Bayt: Bir kullanıcı başka birinin siparişlerini görmüş!

  2. Sen: Kendi bilgisayarımda denedim, olmuyor.

  3. Bayt: Yalnızca site kalabalıkken oluyorsa şüpheli belli: havuzdaki thread'ler.

  4. 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.

Hızlı kontrolBaşlangıç

`static final ThreadLocal<User> CURRENT` alanı `static` olduğu hâlde iki thread aynı anda farklı kullanıcıları nasıl tutabiliyor?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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.

Oda misafirden misafire geçer; çekmeceyi boşaltmazsan yenisi eskiyi bulur.
Adım adım oku
  1. alice'in isteği havuzdaki W1 thread'ine düşer ve çekmeceye, yani ThreadLocal'a kendi adını koyar.
  2. İstek biter, ama remove() çağrılmadığı için çekmece dolu kalır. Thread ölmez, havuza döner.
  3. Sıradaki anonim istek aynı thread'e düşer ve set çağırmadan çekmeceye bakar: alice'i bulur.
  4. Çö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.

CurrentUserFilter.java
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.

Hızlı kontrolOrta

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?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Çoğu zaman doğru çalışan bu filtre, arada bir isteği yanlış tenant'la işliyor. Hatalı satır hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Hatalı satıra dokun, sonra kontrol et.

TenantFilter.java
Java 21UTF-8LF

Kendin gör

Havuzdaki thread neyi hatırlıyor?

Tohum 353793
RequestHandler.java
1static final ThreadLocal<User> CURRENT = new ThreadLocal<>();
2
3void handle(Request req) {
4 if (req.user() != null) CURRENT.set(req.user());
5 orders.list(CURRENT.get()); // null = anonim
6}
Java 21UTF-8LF
  • W1 (istek havuzu)

    boş

  • W2 (istek havuzu)

    boş

  • A1 (audit executor)

    boş

Oynat ya da adımla.

Hız
Adım 0

Ş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.

  1. Varsayılanla oynat. Anonim istek alice’in siparişlerini gördü.
  2. Temizliği “finally” yap. Sızıntı bitti.
  3. Audit’i “Executor’da CURRENT oku” yap. alice ve bob audit kaydından düştü.
  4. Bağlamı ScopedValue yap. Sızıntı yok, ama audit sorunu aynı.
  5. Audit’i “Önce oku, kopyala” yap. Temiz.
Hızlı kontrolOrta

Simülatörde audit görevi executor'da `CURRENT` okuyunca alice ve bob kayıttan düştü; ScopedValue'da da. Neden?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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.

Hızlı kontrolOrta

`ScopedValue.where(CURRENT, alice).run(task)` çağrısı bittikten sonra aynı thread'de `CURRENT.get()` çağrılırsa ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

ScopedValue hangi Java sürümünde preview'dan çıkıp kalıcı oldu?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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.

Hızlı kontrolİleri

Bir `@Async` metodunda `SecurityContextHolder.getContext().getAuthentication()` null dönüyor; controller'da doluydu. En uygun çözüm hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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
Proje dosyaları

src/main/java/bank/context/ CurrentCustomer.java ThreadLocal tutucusu: set ve remove tek yerde.

src/main/java/bank/context/CurrentCustomer.java
public final class CurrentCustomer {
private static final ThreadLocal<String> CUSTOMER_NO = new ThreadLocal<>();
private CurrentCustomer() {}
public static void set(String customerNo) { CUSTOMER_NO.set(customerNo); }
public static String get() { return CUSTOMER_NO.get(); }
public static void clear() { CUSTOMER_NO.remove(); }
}

src/main/java/bank/context/ CurrentCustomerFilter.java Filtre: set ettiği yerde finally ile temizler. Havuzdaki thread bir sonraki müşterinin isteğine temiz gider.

src/main/java/bank/context/CurrentCustomerFilter.java
public class CurrentCustomerFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// Set by the authentication filter from the verified token, never from a raw header.
String customerNo = (String) request.getAttribute("customerNo");
CurrentCustomer.set(customerNo);
try {
chain.doFilter(request, response);
} finally {
// The pooled thread must not carry this customer into the next request:
// that request would see someone else's accounts.
CurrentCustomer.clear();
}
}
}

src/main/java/bank/context/ ContextCopyingExecutor.java Başka bir executor'a geçen iş bağlamı göremez; değer gönderen thread'de yakalanıp çalışan thread'de kurulur.

src/main/java/bank/context/ContextCopyingExecutor.java
public class ContextCopyingExecutor implements Executor {
private final Executor delegate;
public ContextCopyingExecutor(Executor delegate) {
this.delegate = delegate;
}
@Override
public void execute(Runnable task) {
String customerNo = CurrentCustomer.get(); // captured on the submitting thread
delegate.execute(() -> {
CurrentCustomer.set(customerNo); // installed on the worker thread
try {
task.run();
} finally {
CurrentCustomer.clear();
}
});
}
}

src/main/java/bank/context/ RequestContext.java ScopedValue (Java 25): bağ blokla sınırlı, temizlenecek bir şey yok.

src/main/java/bank/context/RequestContext.java
// Java 25: ScopedValue is final. The binding exists only inside run(…).
public final class RequestContext {
public static final ScopedValue<String> USER = ScopedValue.newInstance();
private RequestContext() {}
public static void handle(String user, Runnable request) {
ScopedValue.where(USER, user).run(request); // nothing to clean up afterwards
}
public static String user() {
return USER.isBound() ? USER.get() : "anonymous";
}
}

src/main/java/bank/context/ LeakDemo.java Sızıntıyı gösteren program: tek thread'li havuzda bir müşterinin numarası bir sonraki isteğe taşınıyor; remove ile taşınmıyor.

src/main/java/bank/context/LeakDemo.java
public class LeakDemo {
public static void main(String[] args) throws Exception {
try (var pool = Executors.newFixedThreadPool(1)) { // one thread: every task reuses it
pool.submit(() -> CurrentCustomer.set("C-100042")).get(); // request 1, no cleanup
System.out.println(pool.submit(CurrentCustomer::get).get()); // C-100042 <- request 2 is someone else's!
pool.submit(() -> { // request 3, done right
CurrentCustomer.set("C-200117");
try { /* list accounts */ } finally { CurrentCustomer.clear(); }
}).get();
System.out.println(pool.submit(CurrentCustomer::get).get()); // null <- clean
}
RequestContext.handle("C-300555", () -> System.out.println(RequestContext.user())); // C-300555
System.out.println(RequestContext.user()); // anonymous
}
}

Kendini sına

Şimşek turu1/5

ThreadLocal, aynı değişken için her thread'e ayrı bir değer verir.

Soru 1/2İleri

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?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aklında kalacak üç şey

  1. 1 Havuzdaki thread bir isteği bitirince ölmez, sonrakine geçer. ThreadLocal'a koyduğun değer, remove() edilene kadar orada kalır.
  2. 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. 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.
Sonraki kapı Üç servisi aynı anda çağırıp yalnızca en yavaşı kadar beklemek mümkün mü? CompletableFuture ve Structured Concurrency · 10 dk

5 kart sonraki derste seni bekliyor

0/5 kart bu dersten toplandı

Bu dersin üstüne kurulanlar

Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.