JavaProgramlamaKıdemli Java Geliştirici

**ThreadPoolExecutor**'in **CallerRunsPolicy**'yi sınırlı bir **BlockingQueue** ile kullandığı zaman hangi dairesel bağımlılık kilitlenmesi ortaya çıkar ve görevi içeren **Future.get()**'i çağıran gönderme thread'inin, aynı doygun kuyrukta yer alan sonraki görevlere bağlı olan görevlerin tamamlanmasını beklemesi durumu nedir?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun cevabı

ThreadPoolExecutor, ana thread'leri ve sınırlı kuyruk doygunluğuna ulaştığında, CallerRunsPolicy reddedilen görevi hemen yürütmek için gönderen thread'e devreder. Eğer bu gönderici thread, az önce gönderdiği görevin sonucunu senkronize bir şekilde beklemek için Future.get() çağırmışsa ve gönderilen görevin mantığı içten içe ek görevleri aynı yürütücüye gönderip bunların tamamlanmasını bekliyorsa, dairesel bir bekleme durumu ortaya çıkar.

Gönderen thread, görevi tamamlanana kadar get()'den geri dönemez, ancak görev tamamlanamaz çünkü beklediği alt görevler gerisinde kuyrukta kalır. Kuyruğu boşaltmak için mevcut işçi thread'ler yoktur çünkü hepsi diğer görevlerle meşguldür. Bu durum, gönderici için etkili bir kilitlenmeye yol açar; çünkü yalnızca kuyrukta yer alan alt görevleri (politika aracılığıyla) çalıştırabilen thread ve aynı zamanda bu alt görevlerin tamamlanmasını bekleyip engellenmiştir.

Hayattan bir durum

Bunu, bir ThreadPoolExecutor'un CallerRunsPolicy ile PDF işleme görevlerini gerçekleştirdiği dağıtık bir belge işleme hattında yaşadık. Her belge görevi, meta verileri ayrıştırıp görüntü çıkarma için alt görevler başlattı ve ardından hemen o alt görevler için Future.get() çağrısını yaparak nihai sonucu derlemeye çalıştı.

Yüksek talep altında kuyruk doygun hale geldi ve CallerRunsPolicy, belge görevini web istek işleyici thread'inde yürütmek için tetiklendi. O thread ardından görüntü çıkarma görevlerini gönderdi ve get()'de engellendi; ancak tüm işçi thread'ler diğer belgelerle meşguldü. Yeni alt görevler kuyrukta yer alıyordu ama atanmış değildi.

İşleyici thread, alt görevleri çalıştıramadı çünkü onlara bekliyordu ve alt görevler de çalışamazdı çünkü serbest thread yoktu. Bu, servisi felce uğratan kendini pekiştiren bir kilitlenme oluşturdu; manuel müdahale JVM'yi yeniden başlatana kadar servisi durdurdu.

Aşağıdaki kod, tehlikeli desenin nasıl olduğunu gösterir:

ExecutorService executor = new ThreadPoolExecutor( 2, 2, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.CallerRunsPolicy() ); // Ana istek işleyici thread'inden gönderildi Future<?> parent = executor.submit(() -> { // Havuz doygun olduğunda bu işleyici thread'de çalışır (CallerRunsPolicy) Future<?> child = executor.submit(() -> "çıkarılan görüntü"); // İşleyici thread burada engellenir, çocuğu beklerken // Ama çocuk kuyrukta ve işçi thread'ler serbest değil // İşleyici, engellenmiş olduğu için çocuğu çalıştıramaz return child.get(); }); parent.get(); // Kilitlenme: işleyici thread sonsuza dek bekler

Dört ayrı mimari çözümü değerlendirdik. İlk yaklaşım, CallerRunsPolicy'yi AbortPolicy ile değiştirdi ve istemcide üssel bir geri dönüş deneme döngüsü uyguladı. Bu, çağrı thread'inin mevcut olmasını sağladı ancak geçici hatalar ve karmaşık yeniden deneme mantığı getirerek idempotans garantilerini karmaşıklaştırdı.

İkinci çözüm, doygunluğu tamamen önlemek için sınırsız bir LinkedBlockingQueue'ya genişletildi. Bu reddi ortadan kaldırdı ancak yoğun trafik dalgalanmalarında OutOfMemoryError riski taşıdı ve geri basınç sinyalleri maskelemeye yol açarak açık bir hata yerine aşırı gecikmelere sebep oldu.

Üçüncü seçenek, sınırlı kuyruğu korurken maximumPoolSizecorePoolSize'ın oldukça üzerine çıkardı ve yükü absorbe etmek için thread çoğalmasına güvendi. Bu, aşırı bağlam geçişi ve bellek tüketimi nedeniyle performansı zayıflatan bir durum yarattı; sonuçta işlemcinin önbellek çarpışmasına yol açtı.

Dördüncü yaklaşım, ExecutorCompletionService ve senkronize Future.get() yerine asenkron geri çağırmalar kullanarak iş akışını yapılandırdı. Bu, orijinal belge görevini alt görevler gönderildiğinde işçi thread'i serbest bırakmasına ve yalnızca CompletionService'in tamamlandığına dair sinyal verirken yeniden başlamasına olanak tanıdı.

Dördüncü çözümü seçtik çünkü bu, gönderimi tamamlanmadan fundamental bir şekilde ayırdı. Bu, sınırlı kuyruğun geri basıncını korurken dairesel bekleme durumunu ortadan kaldırmayı sağladı; işçi thread'lerin alt görevleri işlemek üzere geri dönmesini sağlarken orijinal görevin hafif bir durum değişkeninde bildirim beklemesini sağladı.

Bu değişiklik, kilitlenmeleri çözdü, ortalama gecikmeyi %40 azaltarak, zirve yük altında sabit bellek ayak izlerini korudu ve sınırlı kuyruğun hata mantığını feda etmedi.

Adayların Sıklıkla Gözden Kaçırdığı Noktalar

Neden ThreadPoolExecutor sınırsız bir BlockingQueue ile yapılandırıldığında corePoolSize'ın ötesinde yeni thread oluşturmaktan kaçınır?

Yürütücü, yeni thread'ler oluşturmaya yalnızca execute(), görevi bekleyen bir işçi thread'e doğrudan veremezse veya kuyrukta koyamazsa girişiminde bulunur. Sınırsız bir kuyrukta offer() metodu asla false dönmez, bu nedenle yürütücü asla doygunluğa ulaşmadığını algılamaz ve dolayısıyla thread'leri çekirdek sayısının ötesinde tahsis etmez. Bu tasarım, kaynak yönetimi için kuyruklamanın, thread oluşturmanın tercih edilir olduğunu varsayar; ancak bu, havuzun mevcut görünümünün altında, bekleyen işler olmasına rağmen kör bir nokta yaratır. Adaylar sıklıkla, maximumPoolSize'ın kuyruk kapasitesine bakılmaksızın sert bir tavan olarak iş gördüğünü yanlış bir şekilde varsayıyor, oysa kuyrukta sınırlı olma durumu thread büyümesi için kapı bekçisi olarak işlev görmektedir.

Nasıldır CallerRunsPolicy yalnızca bir reddetme yöneticisi olarak değil, aynı zamanda dolaylı bir akış kontrol mekanizması olarak işlemesi?

Görevi gönderen thread'de yürütülerek, politika o thread'in gönderim oranını duraklatmasını ve iş yapmasını zorlar; bu, havuzun işleme kapasitesine eşleştirmek için iç akışın doğal olarak kısıtlanmasını sağlar. Bu geri basınç, çağrı yığınına yukarı doğru yayılır, bu da orijinal üreticiyi durdurur; bu da bu durumu açık bir hız sınırlayıcı kod olmaksızın yavaşlatır. Birçok aday, politikayı yalnızca bırakılan görevler için bir güvenlik önlemi olarak görmektedir, ancak bunun aslında kaynak tükenmesini önlemek için üreticiyi iterek blocklayıcı etkisi olduğu gözden kaçıyor. Bu anlam ayrımını anlamak, gecikmenin kabul edilebilir olduğu sistemler tasarlarken kritik öneme sahiptir.

shutdown() ve CallerRunsPolicy arasındaki ince etkileşim, yürütücü sonlandığında neden zarif bir bozulmayı önlüyor?**

shutdown() çağrıldığında, yürütücü, yeni gönderimlerin RejectedExecutionException aracılığıyla reddedildiği bir duruma geçer ve yapılandırılmış reddetme politikasını tamamen atlar. Adaylar, CallerRunsPolicy'nin kapatma sırasında çağıranda görevleri çalıştırmayı sürdüreceğini varsayıyor, ancak yürütücü, politika hakkında danışmadan önce kapatma durumunu kontrol eder. Bu, zarif bir kapatma aşamasında gönderilen görevlerin hemen başarısız olması ve dolayısıyla bu durumun istasyonun üzerinde çalışmaması için işlevsel gereklilikleri kaybeder; bu da istemcinin istisna ile ilgili hususları ele almasına bağlıdır. Doğru kapanma sırası, kuyruk boşaltmayı gerektirir awaitTermination() veya reddedilen görevleri bir yedekleme yapısına yakalamak için, çünkü politika mekanizması kapatma işareti ayarlandığında devre dışı bırakılır.