JavaProgramlamaJava Arka Uç Geliştirici

**CompletableFuture**'nin örtük **ForkJoinPool** bağımlılığı, engelleyici ağ çağrılarını düzenlerken uygulama verimliliğini nasıl gizlice azaltır?

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

Sorunun cevabı.

CompletableFuture, Java 8'de piyasaya sürüldüğünde, varsayılan eş zamanlı işlemleri ForkJoinPool.commonPool() ile bağlayarak sıfır yapılandırmalı paralellik için optimize edilmişti. Bu tekil yürütücü, Runtime.getRuntime().availableProcessors() - 1 olarak kendini ayarlar; bu hesaplama, gecikme bağımlı işlemler yerine CPU yoğun ve kısa ömürlü görevler için özelleştirilmiştir.

Azalma, geliştiriciler ağ I/O işlerini —HTTP istekleri gibi— supplyAsync() veya thenApplyAsync() aracılığıyla özel bir Executor belirtmeden gönderdiğinde ortaya çıkar. Ortak havuz JVM genelinde paylaşıldığı için, sınırlı iş parçacıklarının engellenmesi sistematik kıtlığa yol açar; tüm iş parçacıkları ağ soketlerinde beklediğinde, CPU ile ilgili görevler (bu dahil Stream paralel boru hatları) ilerleyemez; bu da uygulama verimliliğini etkili bir şekilde dondurur.

Çözüm, açık yürütücü izolasyonu gerektirir. Üretim kodu, bir teklif argümanı kabul eden aşırı yüklemeler aracılığıyla - tercihen sanal iş parçacıkları veya önbellekli bir iş parçacığı havuzu ile I/O için desteklenen- özel bir ExecutorService sağlamalıdır. Bu mimari sınır, engelleyici beklemelerin izole bir ad alanından kaynak tüketmesine olanak tanırken, ortak havuzu hesaplama işlerine müdahale etmeden bırakır.

// Tehlikeli: ForkJoinPool.commonPool()'u örtük kullanır CompletableFuture<String> risky = CompletableFuture.supplyAsync(() -> { // Ortak havuz iş parçacığını engeller! return httpClient.send(request, BodyHandlers.ofString()).body(); }); // Güvenli: Engelleyici I/O için izole yürütücü try (ExecutorService ioExecutor = Executors.newVirtualThreadPerTaskExecutor()) { CompletableFuture<String> safe = CompletableFuture.supplyAsync( () -> httpClient.send(request, BodyHandlers.ofString()).body(), ioExecutor ); }

Hayattan bir durum

Dış REST API'lerden kredi notlarını eş zamanlı olarak çekerek piyasa verilerini zenginleştiren yüksek frekanslı ticaret analitik platformunu düşünün. Orijinal uygulama, varsayılan ortak havuzu kullanarak CompletableFuture.supplyAsync(() -> fetchRating(ticker)) şeklinde binlerce ticker arasında zincirlenmişti. Piyasa dalgalanması sırasında, gecikme felakete yol açacak şekilde yükseldi çünkü on altı çekirdekli bir sunucuda on beş ortak iş parçacığı HTTP zaman aşımı üzerinde engellendi ve tüm uygulamanın paralel veri boru hatlarını dondurarak işlemlerin kaybedilmesine neden oldu.

Düşünülen çözüm: Ortak havuz paralelliğini artırma

Geliştiriciler başlangıçta bloklama beklemeleri karşılamak için -Djava.util.concurrent.ForkJoinPool.common.parallelism=200 ayarını yapmayı önerdiler. Bu yaklaşım, kod değişikliği gerektirmeden anında rahatlama sağladı. Ancak, bu yöntem meşru hesaplama işlerinin CPU ön belleğini aşırı derecede tüketmesine ve gereksiz bekleyen iş parçacıklarının hafızada tutulmasına neden oldu. Temel olarak sürdürülebilir değildir çünkü CPU ve I/O kaynak profillerini tek bir havuzda birleştirir ve sonunda işletim sistemi zamanlayıcısını doymaktadır.

Düşünülen çözüm: get() ile senkron bekleme

Diğer bir alternatif, her kullanılabilir seçenek yaratımından hemen sonra .get() çağrısı yapmaktı; bu, işlemi senkron hale getiriyordu. Bu, ortak havuz aç kalma sorununu ortadan kaldırdı ancak tüm asenkron faydaları ortadan kaldırdı. Kod, seri yürütmeye dönüştü, sunucu kaynaklarını verimsiz hale getirdi ve zirve yükleri sırasında uçtan uca işlem süresini bir büyüklük sırasıyla artırarak düşük gecikmeli SLA'yı doğrudan ihlal etti.

Düşünülen çözüm: I/O için özel esnek yürütücü

Benimsenen strateji, işlemci sayısından bağımsız olarak boyutlandırılan sanal iş parçacıkları (veya ön Loom Java sürümlerinde önbellekli bir iş parçacığı havuzu) kullanan ayrı bir ExecutorService tanıttı. Her asenkron aşama, bu yürütücüyü açıkça thenApplyAsync(transform, ioExecutor) aracılığıyla referans vermektedir. Avantajları arasında, I/O gecikmesinin hesaplamalı verimlilikten tamamen izole edilmesi ve ayrıntılı gözlemlenebilirlik bulunmaktadır. Tek dezavantaj ise yürütücü yaşam döngüsünü ve kapatma kancalarını yönetmek için hafif bir ek iş yüküydü.

Seçilen çözüm ve sonuç

Ekip, Java 21'in Executors.newVirtualThreadPerTaskExecutor() kullanarak özel yürütücü yaklaşımını uyguladı. Bu, engelleyici HTTP gecikmesini CPU ile ilgili analizlerden hemen ayırdı. Sistem verimliliği stres testleri sırasında saniyede elli bin istekte stabil hale gelirken, ortak havuz varyantı binin altına düştü. Gecikme yüzdelikleri %95 oranında düştü ve yürütücü izolasyonunun kritik önemini göstermiş oldu.

Adayların sıklıkla kaçırdığı noktalar


Neden ForkJoinPool boyutu varsayılan olarak availableProcessors() - 1 olarak ayarlanır, fiziksel çekirdek sayısıyla eşleşmez?

Bu çıkarım, bir fiziksel çekirdeği yalnızca çöp toplayıcı ve sistem iş parçacıkları için ayırır; bu da GC duraklamalarının hesaplama görevleriyle rekabet etmesini önler. Adaylar genellikle daha fazla iş parçacığının evrensel olarak performansı artıracağını varsayıyor, ancak bu özel hesaplama, CPU ön belleği süresi ve bağlam geçişini en aza indirmek için optimize edilmiştir. CPU ile ilgili işlerde bu sayıyı aşmak, ön bellek karmaşasına ve zamanlayıcı çatışmasına neden olarak verimliliği azaltır.


Özel bir ForkJoinPool içinde CompletableFuture oluşturursam, neden o özel havuzu kullanmıyor, bunun yerine ortak olanı kullanıyor?

CompletableFuture, nesne yapısı sırasında varsayılan yürütücü referansını ortak havuz tekil olarak katı bir şekilde kodlar; mevcut iş parçacığının yürütme bağlamını incelemez. Bu, asenkron dönüşümlerin her zaman ortak havuza sızmasına neden olur, herhangi bir yürütücü argümanı açıkça geçmedikçe. Geliştiriciler, iş parçacığı yerelliğinin korunduğunu yanlış bir şekilde varsayarlar, bu da görünmeyen havuzlar arası rekabete ve paralel performansı yok eden ön bellek hatlarının zıplamasına yol açar.


Java 21'de sanal iş parçacıkları kullanırken CompletableFuture içindeki bir engelleyici işlem neden beklenmedik bir şekilde taşıyıcı iş parçacığını sabitleyebilir?

Sanal iş parçacıkları üzerinde çalışırken, engelleyici işlemler genellikle sanal iş parçacığını taşıyıcısından sökebilir. Ancak, engelleyici kod synchronized bloğu veya yerel bir yöntem (JNI) içeriyorsa, bu, altındaki platform taşıyıcı iş parçacığını sanal iş parçacığına sabitler. ForkJoinPool bu taşıyıcıları sağlarsa ve hepsi sabitlenirse, havuz, ön-Loom dönemine benzer bir şekilde aç kalır. Adaylar, synchronized anahtar kelimelerinin, sabitleme ve felaket taşıyıcı tükenmesini önlemek için ReentrantLock ile değiştirilmesi gerektiğini kaçırırlar.