@Async ve Thread Havuzu — Max Neden Hiç Dolmuyor?
Önce şunu oku: AOP, Proxy ve Self-Invocation Tuzağı , ThreadLocal ve ScopedValue — Havuzdaki Thread Neyi Hatırlıyor?
30 saniyede özet
@Async bir işi arka plana atar, ama yalnızca başka bir sınıftan çağrılınca. Havuz işi önce kalıcı işçilere, sonra kuyruğa, kuyruk dolunca ek işçilere verir. Kuyruğun sonu yoksa ek işçi hiç çağrılmaz.
Kampanya sabahı hoş geldin e-postaları gecikmeye başladı. Ekip en fazla işçi sayısını (maxPoolSize) 2’den 50’ye çıkardı ve yeniden yayına aldı. Hiçbir şey değişmedi: e-postalar yine aynı hızla, sırayla gidiyordu.
-
Bayt: Kampanya sabahı e-postalar gecikti! En fazla işçi sayısını ikiden elliye çıkardık.
-
Sen: Eee, düzeldi mi?
-
Bayt: Hiçbir şey değişmedi! Sanki kimse yeni işçi çağırmıyor.
-
Bayt: Geçici kuryeyi kim, ne zaman çağırıyor? Önce sen tahmin et.
@Async kimin üzerinden çalışır?
@Async bir işi “sonra yap” diye arka plana bırakır. Spring metodun önüne bir aracı (proxy) koyar; aracı çağrıyı yakalar, işi bir iş dağıtıcısına (TaskExecutorSpring'in iş çalıştırma soyutlaması. @Async metotları varsayılan olarak Boot'un otomatik kurduğu ThreadPoolTaskExecutor'da çalışır: core 8 thread ve sınırsız kuyruk.Sözlükte gör →) verir ve beklemeden döner.
Bu yüzden @Transactional’daki kural burada da geçerli: aynı sınıfın içinden this.sendWelcome() demek proxy’yi atlar. İş istek thread’inde, senkron çalışır; hata da, uyarı da yoktur.
Hiç ayar yapmazsan· istersen atla
Kendi executor’ını tanımlamadıysan Spring Boot bir ThreadPoolTaskExecutor kurar: 8 kalıcı thread ve sonu olmayan bir kuyruk. spring.threads.virtual.enabled=true ise işler virtual thread’lerde çalışır.
Kafam karıştı, daha basit anlat
@Async, işi bir asistana bırakmaktır. Ama aynı sınıfın içinden çağırırsan asistanı atlar ve işi kendin, beklerken yaparsın.
Bir metoda @Async ekledin, ama çağrı hâlâ senkron çalışıyor ve log'daki thread adı http-nio-8080-exec-3. En olası iki neden hangisi?
Spring Boot'ta @EnableAsync açtın ve kendi executor bean'ini tanımlamadın. @Async metotlar nerede çalışır?
Havuz işi nasıl kabul eder?
core 2, max 10, kuyruk sınırsız. Uzun süren 12 iş aynı anda gelirse kaç thread açılır? Cevabı göster
İki. Core dolduktan sonra havuz yeni thread açmak yerine kuyruğu tercih eder. Ek thread yalnızca kuyruk dolunca açılır; sınırsız kuyruk hiç dolmaz.
Adım adım oku
- Gelen iş önce kalıcı işçilere, yani core thread'lere gider: burada iki tane.
- İkisi de meşgulse iş rafa, yani kuyruğa konur.
- Kuyruk sınırsızsa raf hiç dolmaz; max ne kadar yüksek olursa olsun geçici işçi hiç çağrılmaz.
- Kuyruk sınırlıysa raf dolduğunda geçici işçiler max'a kadar gelir ve işi paylaşır.
Bu sıra Spring’in değil, Java’nın ThreadPoolExecutor’ının kuralıdır:
- Çalışan thread sayısı core’dan azsa yeni thread aç.
- Değilse işi kuyruğa koy.
- Kuyruk doluysa max’a kadar ek thread aç.
- O da doluysa işi reddet.
Kafam karıştı, daha basit anlat
Havuz önce sabit çalışanlarını kullanır, sonra işleri sıraya koyar. Ek çalışanı ancak sıra dolunca çağırır. Sıra sınırsızsa ek çalışan hiç gelmez.
ThreadPoolTaskExecutor: corePoolSize 2, maxPoolSize 10, queueCapacity varsayılan (sınırsız). Uzun süren 12 iş aynı anda gelirse kaç thread açılır?
Kendin gör
Max neden hiç dolmuyor?
Tohum 376367Havuz thread'leri — core 2, max 10
Açık thread yok.
Kuyruk — kapasite ∞
Kuyruk boş.
Oynat ya da adımla.
Şu an ne oldu?
core 2, max 10, sınırsız kuyruk
On iki yavaş e-posta işi aynı anda geliyor. Sıra şu: önce core thread, sonra kuyruk, kuyruk dolunca max, o da dolunca ret.
Görevler0/3
Max'ı büyük yaz, ama ek thread açılmasınaçık
İpucu
Sınırsız kuyrukla dene.
Bir işi TaskRejectedException ile kaybetaçık
İpucu
Kuyruğu ve max'ı sınırla.
Hiç iş kaybetmeden taşmayı çağırana yükleaçık
İpucu
Sınırlı havuz, farklı ret politikası.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
- Varsayılanla oynat. Max 10 yazıyor, açılan thread 2; on iş kuyrukta.
- Executor’ı “core 2, max 4, kuyruk 4” yap. Kuyruk dolunca ek thread’ler açıldı, sonra dört iş reddedildi.
- Politikayı “Çağıran çalıştırsın” yap. İş kaybolmadı; istek thread’i yavaşladı.
- “Aynı sınıftan this ile çağır”ı aç. Havuz hiç kullanılmadı.
Kayıt olan kullanıcıya hoş geldin e-postası hiç gitmiyor ve log'da hata yok. Hatalı satır hangisi?
corePoolSize 2, maxPoolSize 4, queueCapacity 4 ve varsayılan ret politikası. Uzun süren 12 iş aynı anda gelirse ne olur?
Havuz dolunca
Sınırsız kuyruk hiçbir şeyi reddetmez; yük sürerse bekleyen işler bellekte birikir. Kuyruğa sınır koyduğunda ise bir karar vermen gerekir: dolunca ne olacak?
Bu kararın adı rejection policyHavuzun thread'leri ve kuyruğu doluyken gelen işe ne olacağını seçen kural. AbortPolicy exception fırlatır; CallerRunsPolicy işi çağıranın thread'inde çalıştırır.Sözlükte gör →. Varsayılan AbortPolicy işi reddeder ve çağırana TaskRejectedException fırlatır. CallerRunsPolicy ise “madem yer yok, sen yap” der: işi çağıranın kendisi yapar, iş kaybolmaz ama gelen istekler yavaşlar.
İkincisi bir backpressureÜretici tüketiciden hızlı olduğunda akışı yavaşlatma mekanizması. Kuyruk büyütmek backpressure değildir — sadece çöküşü erteler.Sözlükte gör → biçimidir: üretici, tüketicinin hızına iner. Hangisinin doğru olduğu işe bağlı; önemli olan seçimi bilerek yapmak.
Kafam karıştı, daha basit anlat
Sıra dolunca bir karar gerekir: yeni işi geri çevirmek ya da işi getirene “o zaman sen yap” demek. İkincisi, getireni yavaşlatarak yükü doğal olarak azaltır.
Her ayarı, havuz dolduğunda yol açtığı sonuca göre ayır.
@Async void bir metot içinde RuntimeException fırladı. Ne olur?
Tuzaklar
Bağlam yeni thread’e geçmez. Arka plandaki iş başka bir thread’de çalışır; dıştaki transaction’ı, giriş yapmış kullanıcıyı (SecurityContext) ve log bağlamını (MDC) görmez. Bunları bir TaskDecorator ile taşı.
Kayıt bitmeden başlayan iş. Arka plandaki iş, az önce kaydettiğin satırı henüz göremeyebilir. İşi @TransactionalEventListener(phase = AFTER_COMMIT) ile kayıttan sonra başlat.
void metotta kaybolan hata. Exception çağırana dönmez; varsayılan olarak yalnızca log’a yazılır. Sonucu önemliyse CompletableFuture döndür.
Her şey tek havuzda. Yavaş rapor üretimi, e-postaları da bekletir. Ağır işlere ayrı, adlandırılmış bir executor ver.
Aşağıdaki örnek bir bankanın transfer servisinden ve bu tuzakların hepsini birlikte çözüyor: para çıktıktan sonra müşteriye SMS gider, ama transfer SMS sağlayıcısını beklemez. Dosyalar arasında sekmelerle gezin.
Derinleş · Transfer sonrası SMS bildirimi: uçtan uca 6 dosya · ~113 satır · ilk okumada atlayabilirsin
Kendini sına
@Async, aynı sınıfın içinden çağrılınca da arka planda çalışır.
@Async metot içinde SecurityContextHolder.getContext().getAuthentication() null dönüyor ve log satırlarında traceId yok. En doğru çözüm hangisi?
Aklında kalacak üç şey
- 1 @Async yalnızca proxy üzerinden gelen çağrıda çalışır. Aynı sınıftan this ile yapılan çağrı, hata vermeden normal sırayla çalışır.
- 2 Havuzun sırası şöyle: kalıcı işçiler, sonra kuyruk, sonra ek işçiler, sonra ret. Kuyruk sınırsızsa ek işçiler hiç kullanılmaz.
- 3 Sınırlı bir kuyruk, dolunca ne olacağına karar vermeyi gerektirir: reddetmek işi kaybettirir, CallerRunsPolicy çağıranı yavaşlatır.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.