İçeriğe geç

@Async ve Thread Havuzu — Max Neden Hiç Dolmuyor?

Orta 9 dk Çok sık karşılaşılır

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

  1. Bayt: Kampanya sabahı e-postalar gecikti! En fazla işçi sayısını ikiden elliye çıkardık.

  2. Sen: Eee, düzeldi mi?

  3. Bayt: Hiçbir şey değişmedi! Sanki kimse yeni işçi çağırmıyor.

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

Hızlı kontrolBaşlangıç

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?

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

Spring Boot'ta @EnableAsync açtın ve kendi executor bean'ini tanımlamadın. @Async metotlar nerede çalışır?

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

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.

Geçici kurye yalnızca raf dolunca çağrılır.
Adım adım oku
  1. Gelen iş önce kalıcı işçilere, yani core thread'lere gider: burada iki tane.
  2. İkisi de meşgulse iş rafa, yani kuyruğa konur.
  3. Kuyruk sınırsızsa raf hiç dolmaz; max ne kadar yüksek olursa olsun geçici işçi hiç çağrılmaz.
  4. 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:

  1. Çalışan thread sayısı core’dan azsa yeni thread aç.
  2. Değilse işi kuyruğa koy.
  3. Kuyruk doluysa max’a kadar ek thread aç.
  4. 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.

Hızlı kontrolOrta

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?

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

Kendin gör

Max neden hiç dolmuyor?

Tohum 376367

Havuz thread'leri — core 2, max 10

Açık thread yok.

Kuyruk — kapasite ∞

Kuyruk boş.

Oynat ya da adımla.

Hız
Adım 0

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

  1. Varsayılanla oynat. Max 10 yazıyor, açılan thread 2; on iş kuyrukta.
  2. Executor’ı “core 2, max 4, kuyruk 4” yap. Kuyruk dolunca ek thread’ler açıldı, sonra dört iş reddedildi.
  3. Politikayı “Çağıran çalıştırsın” yap. İş kaybolmadı; istek thread’i yavaşladı.
  4. “Aynı sınıftan this ile çağır”ı aç. Havuz hiç kullanılmadı.
Hızlı kontrolOrta

Kayıt olan kullanıcıya hoş geldin e-postası hiç gitmiyor ve log'da hata yok. Hatalı satır hangisi?

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

Hatalı satıra dokun, sonra kontrol et.

SignupService.java
Java 21UTF-8LF

corePoolSize 2, maxPoolSize 4, queueCapacity 4 ve varsayılan ret politikası. Uzun süren 12 iş aynı anda gelirse ne olur?

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

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.

Hızlı kontrolOrta

Her ayarı, havuz dolduğunda yol açtığı sonuca göre ayır.

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

Sınıflandırılmamış

Kuyruk bellekte büyür

Hiçbir şey reddedilmez, bekleme uzar.

    İş reddedilir

    Çağıran exception alır.

      Çağıran yavaşlar

      İş kaybolmaz; çağıranın thread'i çalıştırır.

        @Async void bir metot içinde RuntimeException fırladı. Ne olur?

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

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

        src/main/java/com/bank/async/ AsyncConfig.java Bildirimlere ayrı, sınırlı bir havuz; dolunca işi kaybetmek yerine çağıranı yavaşlatır.

        src/main/java/com/bank/async/AsyncConfig.java
        @Configuration
        @EnableAsync
        class AsyncConfig implements AsyncConfigurer {
        private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
        @Bean(name = "notificationExecutor")
        ThreadPoolTaskExecutor notificationExecutor() {
        var executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(200); // bounded: memory stays flat on salary day
        executor.setThreadNamePrefix("notify-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setTaskDecorator(new MdcTaskDecorator());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(20);
        return executor;
        }
        // void @Async methods have no caller to throw to; log them with context.
        // Params may hold phone numbers: log the method only, the MDC carries the trace.
        @Override
        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> log.error("async {} failed", method.getName(), ex);
        }
        }

        src/main/java/com/bank/async/ MdcTaskDecorator.java İsteğin traceId'sini ve müşterisini havuz thread'ine taşır, iş bitince temizler.

        src/main/java/com/bank/async/MdcTaskDecorator.java
        class MdcTaskDecorator implements TaskDecorator {
        @Override
        public Runnable decorate(Runnable task) {
        Map<String, String> context = MDC.getCopyOfContextMap(); // captured on the caller
        return () -> {
        if (context != null) MDC.setContextMap(context);
        try {
        task.run();
        } finally {
        MDC.clear(); // pooled thread: clean up
        }
        };
        }
        }

        src/main/java/com/bank/transfer/ TransferService.java Transferi yapar ve bir olay yayınlar; SMS'i kendisi göndermez.

        src/main/java/com/bank/transfer/TransferService.java
        @Service
        class TransferService {
        private final Ledger ledger;
        private final ApplicationEventPublisher events;
        TransferService(Ledger ledger, ApplicationEventPublisher events) {
        this.ledger = ledger;
        this.events = events;
        }
        @Transactional
        public long transfer(TransferCommand cmd) {
        long entryId = ledger.post(cmd.fromIban(), cmd.toIban(), cmd.amount());
        events.publishEvent(new TransferCompleted(entryId, cmd.customerId(), cmd.fromIban(), cmd.amount()));
        return entryId;
        }
        }
        record TransferCompleted(long entryId, long customerId, String fromIban, BigDecimal amount) {}

        src/main/java/com/bank/notification/ TransferNotifier.java Ayrı bir bean: çağrı proxy'den geçer. Commit'ten sonra, bildirim havuzunda çalışır; geri alınan bir transfer için 'para çıktı' SMS'i gitmez.

        src/main/java/com/bank/notification/TransferNotifier.java
        @Component
        class TransferNotifier {
        private static final Logger log = LoggerFactory.getLogger(TransferNotifier.class);
        private final SmsGateway sms;
        private final CustomerContacts contacts;
        TransferNotifier(SmsGateway sms, CustomerContacts contacts) {
        this.sms = sms;
        this.contacts = contacts;
        }
        // AFTER_COMMIT: the money has really moved before the customer is told so.
        // @Async("notificationExecutor"): the request thread is free as soon as the event is queued.
        @Async("notificationExecutor")
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        public void onTransfer(TransferCompleted event) {
        log.info("notifying customer {} about entry {}", event.customerId(), event.entryId()); // MDC traceId is here
        String masked = "…" + event.fromIban().substring(event.fromIban().length() - 4);
        sms.send(contacts.phoneOf(event.customerId()),
        "Hesabınızdan (%s) %s TL transfer yapıldı.".formatted(masked, event.amount().toPlainString()));
        }
        }

        src/main/java/com/bank/transfer/ TransferController.java İstek, SMS sağlayıcısını beklemeden 201 döner.

        src/main/java/com/bank/transfer/TransferController.java
        @RestController
        @RequestMapping("/transfers")
        class TransferController {
        private final TransferService transfers;
        TransferController(TransferService transfers) {
        this.transfers = transfers;
        }
        @PostMapping
        ResponseEntity<Void> transfer(@Valid @RequestBody TransferCommand cmd) {
        long id = transfers.transfer(cmd);
        return ResponseEntity.created(URI.create("/transfers/" + id)).build(); // SMS still on its way
        }
        }

        src/main/resources/ application.yml Boot'un varsayılan havuzu (etiketsiz @Async) da sınırlanıyor ve kapanışta bekletiliyor.

        src/main/resources/application.yml
        spring:
        task:
        execution:
        thread-name-prefix: app-
        pool:
        core-size: 8
        max-size: 16
        queue-capacity: 500 # default is unbounded
        shutdown:
        await-termination: true
        await-termination-period: 20s

        Kendini sına

        Şimşek turu1/5

        @Async, aynı sınıfın içinden çağrılınca da arka planda çalışır.

        Soru 1/2İleri

        @Async metot içinde SecurityContextHolder.getContext().getAuthentication() null dönüyor ve log satırlarında traceId yok. En doğru çözüm hangisi?

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

        Aklında kalacak üç şey

        1. 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. 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. 3 Sınırlı bir kuyruk, dolunca ne olacağına karar vermeyi gerektirir: reddetmek işi kaybettirir, CallerRunsPolicy çağıranı yavaşlatır.
        Sonraki kapı Önbelleğe aldığın sonuç neden başka bir kullanıcıya da gitti? Spring Cache — @Cacheable Kimin Cevabını Hatırlıyor? · 9 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.