CompletableFuture ve Structured Concurrency
Önce şunu oku: Virtual Threads vs Platform Threads
30 saniyede özet
Üç işi aynı anda başlattın ve biri patladı. allOf diğer ikisini durdurmaz, onlar boşuna çalışmaya devam eder. Structured concurrency tam olarak bu dağınıklığı toplamak için var.
Üç dükkândan aynı anda sipariş verdin ve biri hemen “yok” dedi. Öteki iki kurye ise hâlâ yolda.
Yazılımda da bir istek üç servise paralel gidiyor ve biri patlıyor. Asıl soru hatanın kendisi değil, geride kalanlar.
allOf ile üç çağrı paralel başladı ve biri 30 ms'de patladı. Diğer ikisi ne olur? Cevabı göster
Çoğu kişi “iptal olurlar” der. Olmazlar — çalışmaya, bağlantı tutmaya devam ederler. Hangi executorGörevleri hangi thread'lerin çalıştıracağına karar veren bileşen. Belirtmezsen çoğu asenkron API ortak ForkJoinPool'u kullanır — ve o havuz tüm uygulamayla paylaşılır.Sözlükte gör → üzerinde çalıştıkları da bunu değiştirmez.
Adım adım oku
- Üç çağrı aynı anda başlar.
- Biri erken patlar ve isteğe hata cevabı döner.
- allOf ile öteki iki çağrı cevap döndükten sonra da çalışmaya ve bağlantı tutmaya devam eder.
- StructuredTaskScope ile hata anında öteki iki çağrı da iptal edilir. Sızan iş kalmaz.
-
Bayt: Üç servisi allOf ile aynı anda çağırdım. Biri hemen patladı, ben de hatayı döndüm.
-
Sen: Güzel, iş bitti o zaman.
-
Bayt: Bitmedi! Öteki iki çağrı hâlâ çalışıyor, bağlantı tutuyor, kimse onları beklemiyor.
-
Bayt: Structured concurrency hepsini tek çantaya koyar: çantayı bırakınca içindekiler de iptal olur.
Kendin gör
allOf kardeşleri iptal etmez — structured concurrency neden var
Tohum 8CompletableFuture.allOf
- fetchUserçalışıyor
- fetchOrdersçalışıyor
- fetchRecommendationsçalışıyor
Cevap süresi
30 ms
sırayla olsa 110 ms
Sızan iş
320 ms
cevaptan sonra çalışan
İşler ne zaman durdu
300 ms
iptal yok
Şu an ne oldu?
3 çağrı aynı anda uçuşta — 0 ms
Çağrılar birbirini beklemiyor; toplam süre en yavaş çağrı kadar olacak.
Aklında kalsın: thenCompose zincirlemek paralellik üretmez; paralellik için çağrıların birbirinden bağımsız başlatılması gerekir.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
Üç kurulumu aynı hata senaryosunda karşılaştır:
allOf+Erkenhata. Cevap 30 ms’de döndü ama 320 ms iş sızdı — kırmızı bloklar cevaptan sonra çalışan kısım. İşler 300 ms’ye kadar sürdü.Sıraylaya geç. Sızan iş sıfır — çünkü sonraki çağrılar hiç başlamadı. Ama cevap 110 ms’de geldi.StructuredTaskScope. Cevap yine 30 ms, sızan iş sıfır, iki çağrı iptal edildi.- Hatayı
Patlamasınyap. Üç kurulum arasındaallOfile structured ayırt edilemiyor.
Dördüncü adım önemli: bu iki API’nin farkı mutlu yolda görünmez. Fark yalnızca hata anında ortaya çıkar.
Kafam karıştı, daha basit anlat
Üç arkadaşı aynı anda markete yolladın. Biri “market kapalı” diye döndü. Diğer ikisi hâlâ yolda ve kimse onlara “dönün” demedi.
Her durumda hangi birleştirme metodunu kullanırsın?
Üç yol, üç bedel
| Gecikme | Sızan iş | Hatayı öğrenme | |
|---|---|---|---|
| Sırayla | Toplam (530 ms) | ✅ yok | Geç |
allOf | En yavaş (300 ms) | ❌ 320 ms | Hızlı |
StructuredTaskScope | En yavaş (300 ms) | ✅ yok | Hızlı |
Sıralı çağrının tek avantajı, başlatılmamış işin sızamamasıdır. Structured concurrency bunu paralellikten vazgeçmeden verir — var oluş sebebi budur.
Kafam karıştı, daha basit anlat
Structured concurrency, ekibi tek bir lider altında yollamaktır. Biri başarısız olursa lider herkesi geri çağırır, kimse yolda unutulmaz.
Üç bağımsız servis çağrısını paralel yapıp hepsini beklemek istiyorsun. En temiz yol?
allOf neden iptal etmiyor
var user = supplyAsync(() -> fetchUser(id));var orders = supplyAsync(() -> fetchOrders(id));var recs = supplyAsync(() -> fetchRecommendations(id));
allOf(user, orders, recs).join(); // orders patlarsa burası hemen atarallOf dönen future, kaynaklardan biri patladığında istisnai olarak tamamlanır. Ama user ve recs bağımsız nesnelerdir; allOf onların sahibi değildir, yalnızca izleyicisidir.
Sonuç: join() istisna atar, çağıran metottan çıkar, arka planda iki çağrı hâlâ çalışır — sonuçlarını kimse okumaz.
Üstelik cancel(true) çağırmak da çözmez: CompletableFuture.cancel çalışan görevi kesmez, yalnızca future’ı iptal edilmiş olarak işaretler.
Aşağıdaki örnek bir bankanın döviz masasından: aynı kur üç likidite sağlayıcısına aynı anda soruluyor ve en iyi teklif seçiliyor. İki yaklaşım yan yana.
Derinleş · Üç likidite sağlayıcısından en iyi kur: iki yaklaşım 4 dosya · ~88 satır · ilk okumada atlayabilirsin
StructuredTaskScope.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var user = scope.fork(() -> fetchUser(id)); var orders = scope.fork(() -> fetchOrders(id)); var recs = scope.fork(() -> fetchRecommendations(id));
scope.join(); // hepsi bitene ya da biri patlayana kadar scope.throwIfFailed();
return new Profile(user.get(), orders.get(), recs.get());} // try-with-resources: scope kapanmadan buradan çıkılamazİki garanti veriyor:
- Biri patlarsa diğerleri iptal edilir (
ShutdownOnFailure). - Blok bitmeden hiçbir alt görev hayatta kalamaz.
try-with-resourcesbunu dilin kendisiyle zorunlu kılar.
ShutdownOnSuccess ise tersini yapar: ilk başarılı sonuç gelince kalanları iptal eder — birden çok sağlayıcıya aynı soruyu sorup en hızlısını almak için.
Bu API Java 21’de önizleme olarak geldi ve sonraki sürümlerde şekil değiştirdi; kalıcı olan fikir, imza değil.
`thenApply` ile `thenCompose` arasındaki fark nedir?
Zincirleme ve hata
Klasik soru, tek cümlelik cevap: thenApply map’tir, thenCompose flatMap.
// fonksiyon düz bir değer döndürüyor → thenApplyfuture.thenApply(user -> user.name()); // CompletableFuture<String>
// fonksiyon zaten bir future döndürüyor → thenComposefuture.thenCompose(user -> fetchOrders(user.id())); // CompletableFuture<List<Order>>future.thenApply(user -> fetchOrders(user.id())); // ❌ CompletableFuture<CompletableFuture<...>>thenCombine ise iki bağımsız future’ı birleştirir — zincirleme değil, paralel:
user.thenCombine(orders, Profile::new); // ikisi de paralel çalışırDikkat: thenCompose zincirlemek paralellik üretmez. Her adım bir öncekini bekler; simülatördeki “Sırayla” seçeneği tam olarak budur.
Hata yönetimi. Bir aşama patlarsa, aşağıdaki tüm thenApply adımları atlanır ve istisna ilk exceptionally/handle’a kadar akar.
| Metot | Ne zaman çalışır | Ne döner |
|---|---|---|
exceptionally | Yalnızca hata olduğunda | Yedek değer |
handle | Her durumda | (sonuç, hata) → yeni değer |
whenComplete | Her durumda | Değeri değiştirmez, yalnızca gözlemler |
İki tuzak:
- İstisna sarmalanır.
join()CompletionException,get()ExecutionExceptionatar. Asıl hatagetCause()içindedir. - Kimse beklemezse istisna sessizce kaybolur.
thenAcceptile biten ve hiçjoinedilmeyen bir zincirdeki hata hiçbir yere yazılmaz. Zinciri her zamanexceptionallyveyawhenCompleteile kapat.
Kafam karıştı, daha basit anlat
Sonraki adım düz bir değer döndürüyorsa thenApply yaz. Kendisi de bir future döndürüyorsa thenCompose yaz, yoksa kutunun içinde kutu olur.
Bir `CompletableFuture` zincirinde atılan istisna ne olur?
Tuzaklar
Executor vermezsen supplyAsync ForkJoinPool.commonPool kullanır ve o havuzun boyutu çekirdek sayısı − 1’dir.
Dört çekirdekli bir makinede üç bloklayan I/O çağrısı havuzu tamamen doldurur ve parallelStream dahil aynı havuzu kullanan her şey durur.
supplyAsync(() -> blockingCall(), ioExecutor); // I/O için her zaman kendi executor'ınJava 21 ile daha iyi bir cevap var: Executors.newVirtualThreadPerTaskExecutor(). Virtual thread bloklandığında carrier threadBir virtual thread'i o an çalıştıran gerçek OS thread'i. Virtual thread blokladığında taşıyıcı serbest kalır ve başka bir virtual thread'e geçer.Sözlükte gör → thread’i serbest bırakır, dolayısıyla havuz doygunluğu sorunu ortadan kalkar.
Kendini sına
Önce hızlı bir ısınma: puan yok, kayıt yok.
allOf, işlerden biri patlayınca ötekileri iptal eder.
Üç `CompletableFuture` `allOf` ile bekleniyor ve ikincisi 30 ms'de patlıyor. Diğer ikisine ne olur?
Aklında kalacak üç şey
- 1 allOf sadece bekler: bir iş patlayınca hata verir ama diğer işleri iptal etmez, onlar çalışmaya devam eder.
- 2 Structured concurrency'nin getirdiği şey hız değil, düzen: hiçbir iş onu başlatan istekten uzun yaşamaz.
- 3 thenApply ile thenCompose farkı map ile flatMap farkıdır. Yanlışını seçersen elinde iç içe iki future kalır.
5 kart sonraki derste seni bekliyor