CompletableFuture ve Asenkron Programlama
Bir web uygulaması düşün: kullanıcı profil sayfasını açtığında kullanıcı bilgisi, sipariş geçmişi ve önerilen ürünler olmak üzere üç farklı servisten veri çekmen gerekiyor. Her biri 200ms sürüyor. Sırayla çağırırsan 600ms beklersin. Ama üçünü aynı anda başlatırsan? 200ms'de biter.
İşte CompletableFuture, Java'da bu tür asenkron ve paralel işlemleri yönetmenin modern yoludur. Java 8'de tanıtıldı, Java 9+ ile güçlendi ve bugün reactive olmayan projelerde asenkron programlamanın temel taşıdır. Bu derste CompletableFuture'ı sıfırdan öğrenecek, zincirleme, birleştirme, hata yönetimi ve gerçek dünya senaryolarını inceleyeceğiz.
1. Future vs CompletableFuture
Java 5'te gelen Future arayüzü, asenkron işlemin sonucunu temsil eder. Ama ciddi sınırlamaları var.
import java.util.concurrent.*;
class Main {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Sonuç hazır";
});
// Sonucu almak için bloklanmak zorundasın
String result = future.get(); // 1 saniye bekler — thread bloklanır
System.out.println(result);
executor.shutdown();
}
}Future.get() çağrısı bloklayıcıdır — sonuç gelene kadar thread orada takılır. İki Future'ın sonucunu birleştirmek, birinin sonucuna göre diğerini başlatmak, hata durumunda alternatif değer döndürmek... bunların hiçbiri Future ile doğrudan yapılamaz.
🎯 Analoji — Telefon Siparişi:
>
Future, 90'lardaki telefonla sipariş gibidir. Mağazayı ararsın, sipariş verirsin, sana "hazır olunca gel al" derler. Ama ne zaman hazır olacağını bilemezsin — kapının önünde oturup beklemelisin (get()). Haber veremezler, başka işle zincirleme yapamazsın.
>
CompletableFutureise online sipariş gibidir. Sipariş verirsin, kargoya verilince SMS gelir, kapıya gelince bildirim alırsın. Her adımda otomatik aksiyonlar tanımlayabilirsin: "Kargoya verilince bana mail at", "Teslim edilince puanla", "Sorun olursa iade başlat". Hiçbir yerde oturup beklemek yok — her şey olaya bağlı, zincirleme ve non-blocking.
Fark Tablosu
| Özellik | Future | CompletableFuture |
|---|---|---|
| Sonucu alma | get() — bloklayıcı | thenApply() — non-blocking |
| Zincirleme | ❌ | ✅ thenApply, thenCompose |
| Birleştirme | ❌ | ✅ thenCombine, allOf |
| Hata yönetimi | try-catch (get'te) | exceptionally(), handle() |
| Manuel tamamlama | ❌ | ✅ complete(), completeExceptionally() |
| Callback | ❌ | ✅ thenAccept, whenComplete |
| Timeout | ❌ | ✅ orTimeout() (Java 9+) |
2. CompletableFuture Oluşturma
supplyAsync() — Değer Döndüren Asenkron İşlem
En sık kullanacağın metod budur. Bir iş parçacığında çalışıp sonuç döndürür.
import java.util.concurrent.*;
class Main {
public static void main(String[] args) throws Exception {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Çalışan thread: " + Thread.currentThread().getName());
sleep(500);
return "Kullanıcı verisi yüklendi";
});
System.out.println("Ana thread bloklanmadı, başka iş yapabilirim");
String result = future.join(); // veya get() — sonucu almak için
System.out.println(result);
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}supplyAsync(), varsayılan olarak ForkJoinPool.commonPool() thread havuzunu kullanır. Bir Supplier<T> alır ve CompletableFuture<T> döndürür.
runAsync() — Değer Döndürmeyen Asenkron İşlem
Sonuca ihtiyacın yoksa — örneğin loglama, bildirim gönderme gibi yan etkiler için — runAsync() kullanılır.
CompletableFuture<Void> logFuture = CompletableFuture.runAsync(() -> {
System.out.println("Kullanıcı girişi loglanıyor...");
sleep(200);
System.out.println("Log kaydedildi");
});
logFuture.join();runAsync() bir Runnable alır ve CompletableFuture<Void> döndürür. Sonuç yoktur, sadece işin bittiğini garanti edersin.
completedFuture() — Anında Hazır Değer
Test veya cache senaryolarında, zaten elinde olan bir değeri CompletableFuture olarak sarmak için kullanılır.
CompletableFuture<String> cached = CompletableFuture.completedFuture("Cache'ten gelen veri");
System.out.println(cached.join()); // "Cache'ten gelen veri" — beklemek yok3. Zincirleme — thenApply, thenAccept, thenRun
CompletableFuture'ın gücü zincirleme yeteneğindedir. Bir işlem bitince otomatik olarak bir sonraki adıma geçersin — callback cehennemine düşmeden, okunaklı bir pipeline kurarsın.
thenApply() — Dönüştür ve Yeni Değer Döndür
Bir önceki adımın sonucunu alır, dönüştürür ve yeni bir CompletableFuture döndürür. Stream'deki map() gibi düşün.
import java.util.concurrent.*;
class Main {
public static void main(String[] args) {
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> " Merhaba Dünya ")
.thenApply(String::trim)
.thenApply(String::toUpperCase)
.thenApply(s -> s + "!");
System.out.println(result.join()); // "MERHABA DÜNYA!"
}
}Her thenApply() bir Function<T, R> alır — önceki sonucu girdi olarak alır, yeni değer üretir.
thenAccept() — Sonucu Tüket, Bir Şey Döndürme
Sonucu ekrana yazdırmak, veritabanına kaydetmek gibi terminal operasyonlar için kullanılır. Consumer<T> alır, CompletableFuture<Void> döndürür.
CompletableFuture.supplyAsync(() -> fetchUserName(userId))
.thenApply(name -> "Hoşgeldin, " + name)
.thenAccept(System.out::println); // "Hoşgeldin, Tolga"thenRun() — Önceki Sonuçla İlgilenme, Sadece Bir Şey Yap
Önceki adımın sonucunu almaz bile — sadece "iş bitti, şimdi şunu yap" demek istediğinde kullanırsın.
CompletableFuture.supplyAsync(() -> saveOrder(order))
.thenRun(() -> System.out.println("Sipariş kaydedildi, bildirim gönderiliyor..."))
.thenRun(() -> sendNotification(order.getUserId()));Async Varyantları
Her zincirleme metodun bir de Async varyantı var: thenApplyAsync(), thenAcceptAsync(), thenRunAsync(). Normal varyantlar genellikle bir önceki adımın thread'inde çalışır; Async varyantlar ise işi yeni bir thread'e gönderir.
CompletableFuture.supplyAsync(() -> heavyComputation())
.thenApplyAsync(result -> anotherHeavyWork(result)) // farklı thread'de çalışır
.thenAccept(System.out::println);Kural basit: sonraki adım da ağırsa Async kullan, hafifse normal versiyonu yeterli.
4. Birleştirme — İki veya Daha Fazla Future'ı Bir Araya Getirme
Gerçek uygulamalarda birden fazla asenkron işlemi birleştirmen gerekir. CompletableFuture bunun için güçlü araçlar sunar.
thenCombine() — İki Bağımsız Sonucu Birleştir
İki CompletableFuture bağımsız çalışır, ikisi de tamamlanınca sonuçları birleştirilir.
import java.util.concurrent.*;
class Main {
public static void main(String[] args) {
CompletableFuture<Double> priceFuture = CompletableFuture.supplyAsync(() -> {
sleep(300);
return 149.99; // Ürün fiyatı
});
CompletableFuture<Double> discountFuture = CompletableFuture.supplyAsync(() -> {
sleep(200);
return 0.15; // %15 indirim
});
CompletableFuture<String> finalPrice = priceFuture.thenCombine(
discountFuture,
(price, discount) -> String.format("Final fiyat: %.2f TL", price * (1 - discount))
);
System.out.println(finalPrice.join()); // "Final fiyat: 127.49 TL"
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}thenCombine(), iki future da tamamlanınca BiFunction ile sonuçları birleştirir. İki çağrı paralel çalışır — toplam süre ikisinden uzun olanı kadardır (300ms).
thenCompose() — Sıralı Bağımlı İşlemler
Bir future'ın sonucu diğerinin girdisiyse — yani ardışık bağımlılık varsa — thenCompose() kullanılır. Stream'deki flatMap() gibi düşün.
import java.util.concurrent.*;
class Main {
public static void main(String[] args) {
CompletableFuture<String> result = getUserId("tolga@mail.com")
.thenCompose(userId -> getOrderHistory(userId)) // userId'ye bağımlı
.thenCompose(orders -> calculateTotal(orders)); // orders'a bağımlı
System.out.println(result.join());
}
static CompletableFuture<Long> getUserId(String email) {
return CompletableFuture.supplyAsync(() -> {
sleep(100);
return 42L;
});
}
static CompletableFuture<String> getOrderHistory(Long userId) {
return CompletableFuture.supplyAsync(() -> {
sleep(150);
return "3 sipariş bulundu";
});
}
static CompletableFuture<String> calculateTotal(String orders) {
return CompletableFuture.supplyAsync(() -> {
sleep(100);
return "Toplam: 1,250 TL";
});
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}thenApply vs thenCompose farkı: thenApply() bir değer döndürür (Function<T, R>), thenCompose() bir CompletableFuture döndürür (Function<T, CompletableFuture<R>>). Eğer dönüştürme fonksiyonun asenkron ise thenCompose(), senkron ise thenApply() kullan.
allOf() — Hepsi Tamamlanınca
Birden fazla bağımsız future'ın hepsinin bitmesini bekler.
import java.util.concurrent.*;
import java.util.stream.*;
import java.util.List;
class Main {
public static void main(String[] args) {
long start = System.currentTimeMillis();
CompletableFuture<String> user = CompletableFuture.supplyAsync(() -> {
sleep(300); return "Kullanıcı: Tolga";
});
CompletableFuture<String> orders = CompletableFuture.supplyAsync(() -> {
sleep(400); return "Siparişler: 5 adet";
});
CompletableFuture<String> recommendations = CompletableFuture.supplyAsync(() -> {
sleep(250); return "Öneriler: 10 ürün";
});
CompletableFuture<Void> allDone = CompletableFuture.allOf(user, orders, recommendations);
allDone.join();
// Tüm sonuçları topla
System.out.println(user.join());
System.out.println(orders.join());
System.out.println(recommendations.join());
long elapsed = System.currentTimeMillis() - start;
System.out.println("Toplam süre: ~" + elapsed + "ms"); // ~400ms (en uzunun süresi)
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}allOf() bir CompletableFuture<Void> döndürür — sonuçları kendin toplamalısın. Üç servis paralel çağrılır ve toplam süre en yavaş servis kadardır.
anyOf() — Herhangi Biri Tamamlanınca
Birden fazla kaynaktan aynı veriyi çekerken, ilk gelen kazanır mantığı için kullanılır.
CompletableFuture<Object> fastest = CompletableFuture.anyOf(
fetchFromCache(), // 50ms
fetchFromDatabase(), // 200ms
fetchFromRemoteApi() // 500ms
);
System.out.println("İlk gelen: " + fastest.join());anyOf(), CompletableFuture<Object> döndürür — tip bilgisi kaybolur, cast etmen gerekir. Genellikle aynı türde future'lar arasında yarış yaptırmak için kullanılır.
5. Hata Yönetimi
Asenkron kodda hata yönetimi senkron koddan farklıdır. try-catch ile future'ın içindeki hatayı yakalayamazsın çünkü hata farklı bir thread'de olur. CompletableFuture bunu çözmek için üç metod sunar.
exceptionally() — Hata Olursa Varsayılan Değer
Zincirdeki herhangi bir adımda hata olursa yakalanır ve alternatif değer döndürülür.
import java.util.concurrent.*;
class Main {
public static void main(String[] args) {
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> {
if (true) throw new RuntimeException("API erişilemez");
return "veri";
})
.thenApply(data -> "İşlenmiş: " + data)
.exceptionally(ex -> {
System.out.println("Hata yakalandı: " + ex.getMessage());
return "Varsayılan veri";
});
System.out.println(result.join()); // "Varsayılan veri"
}
}exceptionally() sadece hata varsa çalışır. Hata yoksa atlanır. Zincirin herhangi bir adımındaki hatayı yakalar — en alta koymak best practice'tir.
handle() — Hem Başarı Hem Hata
Sonuç ne olursa olsun (başarı veya hata) çalışır. BiFunction<T, Throwable, R> alır.
import java.util.concurrent.*;
class Main {
public static void main(String[] args) {
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> {
// Bazen başarılı, bazen hatalı
if (Math.random() > 0.5) throw new RuntimeException("Timeout");
return "API yanıtı";
})
.handle((data, ex) -> {
if (ex != null) {
System.out.println("Hata: " + ex.getMessage());
return "Cache'ten veri";
}
return "Taze veri: " + data;
});
System.out.println(result.join());
}
}handle(), exceptionally() ile thenApply()'ın birleşimi gibidir. Her iki durumu da tek bir yerde kontrol edersin.
whenComplete() — Gözlemle Ama Sonucu Değiştirme
Sonucu loglamak, metrik toplamak gibi yan etkiler için kullanılır. Sonucu dönüştürmez — sadece gözlemler.
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> "İşlem tamam")
.whenComplete((data, ex) -> {
if (ex != null) {
logger.error("İşlem başarısız", ex);
} else {
logger.info("İşlem başarılı: {}", data);
}
});
// whenComplete, sonucu değiştirmez — orijinal değer/hata aynen devam ederKarşılaştırma
| Metod | Çalışma Zamanı | Sonucu Değiştirir? | Kullanım |
|---|---|---|---|
exceptionally() | Sadece hata varsa | Evet (hata → değer) | Fallback değer |
handle() | Her zaman | Evet | Hem başarı hem hata |
whenComplete() | Her zaman | Hayır | Loglama, metrik |
⚠️ Dikkat:
join()veyaget()çağrısında hata varsa exception fırlatılır.join()→CompletionException(unchecked),get()→ExecutionException(checked). Geneldejoin()tercih edilir çünkü checked exception ile uğraşmazsın.
6. Timeout Yönetimi (Java 9+)
Asenkron işlemler sonsuza kadar bekleyemez. Java 9 ile gelen timeout metodları bu sorunu zarif şekilde çözer.
orTimeout() — Süre Aşarsa Hata Fırlat
import java.util.concurrent.*;
class Main {
public static void main(String[] args) {
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> {
sleep(3000); // 3 saniye süren yavaş servis
return "Yavaş sonuç";
})
.orTimeout(1, TimeUnit.SECONDS) // 1 saniyede bitmezse hata
.exceptionally(ex -> {
System.out.println("Timeout! " + ex.getMessage());
return "Timeout varsayılan değer";
});
System.out.println(result.join()); // "Timeout varsayılan değer"
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}orTimeout(), belirlenen sürede tamamlanmazsa TimeoutException fırlatır. exceptionally() ile birleştirerek güvenli fallback sağlarsın.
completeOnTimeout() — Süre Aşarsa Varsayılan Değer Kullan
Hata yerine doğrudan varsayılan bir değer döndürür — daha sessiz bir yaklaşım.
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> {
sleep(5000);
return "Yavaş servis sonucu";
})
.completeOnTimeout("Cache'ten eski veri", 1, TimeUnit.SECONDS);
System.out.println(result.join()); // "Cache'ten eski veri"completeOnTimeout(), exception fırlatmak yerine verdiğin değeri kullanır. Kullanıcı deneyimini bozmadan graceful degradation sağlar.
7. Custom Executor ile Thread Pool Kontrolü
Varsayılan ForkJoinPool.commonPool(), CPU çekirdek sayısı - 1 kadar thread tutar. I/O-yoğun işlemlerde (API çağrısı, veritabanı sorgusu) bu yeterli olmaz — thread'lerin çoğu bekleyerek zaman harcar. Bu durumda kendi thread pool'unu tanımlarsın.
import java.util.concurrent.*;
class Main {
public static void main(String[] args) throws Exception {
// I/O işlemleri için daha geniş bir pool
ExecutorService ioPool = Executors.newFixedThreadPool(20);
CompletableFuture<String> apiCall = CompletableFuture.supplyAsync(() -> {
System.out.println("API thread: " + Thread.currentThread().getName());
sleep(500);
return "API yanıtı";
}, ioPool); // İkinci parametre: custom executor
CompletableFuture<String> dbQuery = CompletableFuture.supplyAsync(() -> {
System.out.println("DB thread: " + Thread.currentThread().getName());
sleep(300);
return "DB sonucu";
}, ioPool);
String combined = apiCall.thenCombine(dbQuery, (api, db) -> api + " + " + db).join();
System.out.println(combined);
ioPool.shutdown();
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}Her supplyAsync() ve runAsync() çağrısı ikinci parametre olarak Executor alabilir. I/O işlemleri için geniş thread pool, CPU işlemleri için dar pool kullanmak iyi bir pratiktir.
💡 İpucu: I/O-yoğun işler için thread sayısını
çekirdek sayısı * (1 + bekleme süresi / işlem süresi)formülüyle hesapla. Örneğin 8 çekirdek, 200ms bekleme, 50ms işlem: 8 × (1 + 200/50) = 40 thread makul bir başlangıçtır. CPU-yoğun işler için çekirdek sayısı kadar thread yeterlidir.
8. Gerçek Dünya: E-Ticaret Sipariş Özeti
Birden fazla servisten paralel veri çekip birleştiren gerçek bir senaryo yazalım.
import java.util.concurrent.*;
import java.util.*;
class Main {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
long start = System.currentTimeMillis();
// 1. Kullanıcı bilgisi
CompletableFuture<Map<String, String>> userFuture = CompletableFuture.supplyAsync(() -> {
sleep(250);
return Map.of("name", "Tolga", "email", "tolga@mail.com", "tier", "Gold");
}, pool);
// 2. Sepet bilgisi
CompletableFuture<List<String>> cartFuture = CompletableFuture.supplyAsync(() -> {
sleep(200);
return List.of("MacBook Pro - 45000 TL", "AirPods - 5000 TL", "Magic Mouse - 2500 TL");
}, pool);
// 3. Kargo ücreti (kullanıcı tier'ına bağımlı — sıralı)
CompletableFuture<Double> shippingFuture = userFuture.thenApplyAsync(user -> {
sleep(150);
return user.get("tier").equals("Gold") ? 0.0 : 49.99;
}, pool);
// 4. İndirim oranı (paralel)
CompletableFuture<Double> discountFuture = CompletableFuture.supplyAsync(() -> {
sleep(180);
return 0.10; // %10 kampanya indirimi
}, pool);
// Tüm sonuçları birleştir
CompletableFuture<String> orderSummary = CompletableFuture
.allOf(userFuture, cartFuture, shippingFuture, discountFuture)
.thenApply(v -> {
Map<String, String> user = userFuture.join();
List<String> cart = cartFuture.join();
double shipping = shippingFuture.join();
double discount = discountFuture.join();
double subtotal = 52500.0; // basitleştirme
double total = subtotal * (1 - discount) + shipping;
StringBuilder sb = new StringBuilder();
sb.append("=== SİPARİŞ ÖZETİ ===\n");
sb.append("Müşteri: ").append(user.get("name"))
.append(" (").append(user.get("tier")).append(")\n");
sb.append("Ürünler:\n");
cart.forEach(item -> sb.append(" • ").append(item).append("\n"));
sb.append("Ara toplam: ").append(String.format("%.2f", subtotal)).append(" TL\n");
sb.append("İndirim: %").append((int)(discount * 100)).append("\n");
sb.append("Kargo: ").append(shipping == 0 ? "Ücretsiz (Gold)" :
String.format("%.2f TL", shipping)).append("\n");
sb.append("TOPLAM: ").append(String.format("%.2f", total)).append(" TL");
return sb.toString();
});
System.out.println(orderSummary.join());
long elapsed = System.currentTimeMillis() - start;
System.out.println("\nToplam süre: ~" + elapsed + "ms");
// Sıralı olsa: 250+200+150+180 = 780ms
// Paralel: ~400ms (user 250ms + shipping 150ms en uzun sıralı zincir)
pool.shutdown();
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}Bu örnekte kullanıcı ve sepet bilgisi paralel çekilir. Kargo ücreti kullanıcı bilgisine bağımlıdır, bu yüzden thenApplyAsync() ile zincirlenir. İndirim oranı bağımsızdır, paralel çalışır. Sonunda allOf() ile hepsi birleştirilir.
9. CompletableFuture vs ExecutorService vs Virtual Threads
Java'da asenkron programlamanın üç farklı yaklaşımı var. Hangisini ne zaman kullanmalısın?
ExecutorService — Klasik Thread Pool
ExecutorService executor = Executors.newFixedThreadPool(10);
Future<String> future = executor.submit(() -> "Sonuç");
String result = future.get(); // Bloklayıcı
executor.shutdown();Basit "at ve unut" veya "at ve bekle" senaryolarında yeterli. Zincirleme ve birleştirme yoksa ExecutorService daha basittir.
CompletableFuture — Asenkron Pipeline
CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(this::transform)
.thenCompose(this::saveAsync)
.exceptionally(ex -> fallbackValue)
.thenAccept(this::notify);Zincirleme, birleştirme, hata yönetimi gereken senaryolarda CompletableFuture kullan. Özellikle birden fazla asenkron adımın birbirine bağlı olduğu pipeline'larda rakipsizdir.
Virtual Threads (Java 21+) — Hafif Thread'ler
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> future = executor.submit(() -> {
Thread.sleep(1000); // Bloklayıcı ama sorun değil — virtual thread ucuz
return "Sonuç";
});
System.out.println(future.get());
}Virtual thread'ler bloklayıcı kodu ucuz yapar. Thread oluşturma maliyeti çok düşüktür (birkaç KB stack), milyonlarcasını aynı anda çalıştırabilirsin. Bloklayıcı I/O yapan basit iş parçacıkları için idealdir.
Karşılaştırma Tablosu
| Özellik | ExecutorService | CompletableFuture | Virtual Threads |
|---|---|---|---|
| Java sürümü | 5+ | 8+ | 21+ |
| Programlama stili | Bloklayıcı | Non-blocking/callback | Bloklayıcı |
| Zincirleme | ❌ | ✅ | ❌ (gerekmez) |
| Birleştirme | ❌ | ✅ | Manuel |
| Hata yönetimi | try-catch | exceptionally/handle | try-catch |
| Thread maliyeti | Ağır (~1MB stack) | Ağır (pool paylaşır) | Hafif (~KB) |
| En uygun senaryo | Basit paralel iş | Kompleks pipeline | Yüksek eşzamanlılık, basit I/O |
💡 İpucu: Virtual thread'ler CompletableFuture'ın yerini almaz — farklı sorunları çözerler. Pipeline ve zincirleme mantığı gerekiyorsa CompletableFuture hâlâ en iyi seçim. Binlerce bağımsız I/O işlemi varsa virtual thread'ler mükemmel. İkisini birlikte de kullanabilirsin: CompletableFuture pipeline'ını virtual thread executor ile çalıştırabilirsin.
10. Yaygın Hatalar ve Best Practices
Hata 1: join() Çağrısını Yanlış Yerde Yapmak
// ❌ YANLIŞ — Parallellik yok! Her adımda bloklanıyor
List<String> results = userIds.stream()
.map(id -> CompletableFuture.supplyAsync(() -> fetchUser(id)).join()) // hemen bekler
.collect(Collectors.toList());
// ✅ DOĞRU — Önce tüm future'ları başlat, sonra sonuçları topla
List<CompletableFuture<String>> futures = userIds.stream()
.map(id -> CompletableFuture.supplyAsync(() -> fetchUser(id)))
.collect(Collectors.toList());
List<String> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());Hata 2: Common Pool'da I/O İşlemi Yapmak
// ❌ YANLIŞ — Common pool CPU işleri için, I/O ile bloklanır
CompletableFuture.supplyAsync(() -> httpClient.get(url)); // common pool'u tıkar
// ✅ DOĞRU — I/O işleri için ayrı pool kullan
ExecutorService ioPool = Executors.newFixedThreadPool(20);
CompletableFuture.supplyAsync(() -> httpClient.get(url), ioPool);ForkJoinPool.commonPool() genelde CPU çekirdek sayısı - 1 thread tutar. I/O bekleyen işler bu thread'leri meşgul eder ve uygulamanın tamamını yavaşlatır.
Hata 3: Hata Yönetimini Unutmak
// ❌ YANLIŞ — Hata sessizce yutulur
CompletableFuture.supplyAsync(() -> riskyOperation())
.thenAccept(System.out::println);
// Hata olursa hiçbir yere loglanmaz!
// ✅ DOĞRU — Her zaman hata yönetimi ekle
CompletableFuture.supplyAsync(() -> riskyOperation())
.thenAccept(System.out::println)
.exceptionally(ex -> {
System.err.println("Hata: " + ex.getMessage());
return null;
});Best Practice Özet
I/O işlemleri için her zaman custom executor kullan — common pool'u I/O ile tıkama
Her pipeline'a hata yönetimi ekle —
exceptionally()veyahandle()en sona koyTimeout koy —
orTimeout()ile sonsuz beklemeyi engellejoin() çağrısını pipeline sonuna bırak — erken join parallelliği öldürür
Executor'ı kapat — shutdown() veya try-with-resources (Java 19+ AutoCloseable)
11. Özet
CompletableFuture, Java'da asenkron programlamanın temel aracıdır.
Future'ın aksine zincirleme, birleştirme ve non-blocking hata yönetimi sunar.supplyAsync() değer döndüren, runAsync() değer döndürmeyen asenkron işlemler başlatır. İkinci parametre olarak custom
Executoralabilirler.Zincirleme için
thenApply()(dönüştür),thenAccept()(tüket),thenRun()(sadece çalıştır) kullanılır. Senkron dönüşümthenApply, asenkron dönüşümthenComposeile yapılır.Birleştirme için
thenCombine()(iki bağımsız sonuç),allOf()(hepsi),anyOf()(ilk gelen) kullanılır.Hata yönetimi için
exceptionally()(fallback),handle()(her iki durum),whenComplete()(gözlem) vardır. Her pipeline'a mutlaka hata yönetimi eklenmeli.Timeout için Java 9+ ile gelen
orTimeout()vecompleteOnTimeout()kullanılır. Production'da her dış çağrıya timeout koymak zorunluluktur.
AI Asistan
Sorularını yanıtlamaya hazır