Java'da CompletableFuture ile Asenkron Programlama
Java'da CompletableFuture ile Asenkron Programlama
Diyelim ki bir e-ticaret uygulaması geliştiriyorsun. Kullanıcı bir ürün sayfasını açtığında arka planda şunlar oluyor: ürün bilgisi veritabanından çekiliyor, stok durumu envanter servisinden sorgulanıyor, kullanıcının geçmiş siparişlerine göre öneriler hesaplanıyor ve fiyat bilgisi döviz servisinden güncelleniyor. Bu dört işlemi sırayla yaparsan, her biri 200ms sürse bile toplam 800ms beklersin. Ama hepsi birbirinden bağımsız — neden paralel çalıştırmayasın?
İşte tam burada CompletableFuture sahneye çıkıyor. Java 8 ile hayatımıza giren bu yapı, asenkron programlamayı callback cehenneminden kurtarıp okunabilir, zincirleme bir hale getiriyor. Bu yazıda CompletableFuture'ı sıfırdan, derinlemesine öğreneceksin — sadece API'yi değil, neden böyle tasarlandığını ve gerçek projelerde nasıl kullanılacağını anlayacaksın.
Senkron vs Asenkron: Temel Fark
Bunu bir restoran analojisiyle düşün. Senkron çalışma, garsonun her masaya teker teker gidip sipariş alması, mutfağa iletmesi, yemeği beklemesi ve masaya götürmesi gibi. Bir masa bitene kadar diğerleri bekler. Asenkron çalışma ise garsonun tüm masaların siparişini alması, hepsini mutfağa iletmesi ve hazır olanı götürmesi. Garson boşa beklemez.
Java'da senkron kod yazarken her satır bir öncekinin bitmesini bekler:
// Senkron yaklaşım — her çağrı sırayla, hepsi birbirini bekler
String urunBilgisi = urunServisi.getUrun(urunId); // 200ms
int stokDurumu = stokServisi.getStok(urunId); // 200ms
List<String> oneriler = oneriServisi.getOneriler(userId); // 200ms
double fiyat = dovizServisi.getFiyat(urunId, "TRY"); // 200ms
// Toplam: ~800ms — kullanıcı bekliyorAsenkron yaklaşımda ise bu dört işlemi aynı anda başlatır, hepsi bittiğinde sonucu birleştirirsin. Toplam süre en yavaş olanın süresi kadar olur: ~200ms.
Future'ın Yetersizliği
Java 5'te gelen Future arayüzü asenkron programlamanın ilk adımıydı. Ama ciddi eksiklikleri vardı:
// Eski usul Future — bloklayan, zincirlenemeyen, hata yönetimi zor
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(() -> {
// Uzun süren bir işlem
Thread.sleep(1000);
return "Sonuç hazır";
});
// future.get() çağırana kadar thread bloklanır!
String sonuc = future.get(); // Bu satır 1 saniye beklerFuture.get() çağırdığın an thread bloklanır ve asenkronluğun tüm avantajı yok olur. Ayrıca Future'ları zincirleyemezsin — "önce şunu yap, sonra onun sonucuyla bunu yap" diyemezsin. Hata yönetimi de try-catch ile yapılır, fonksiyonel bir yaklaşım yoktur.
CompletableFuture: Modern Çözüm
CompletableFuture<T>, Future<T> arayüzünü implemente eder ama çok daha fazlasını sunar. İsmi "tamamlanabilir gelecek" anlamına gelir — yani hem asenkron bir sonucu temsil eder, hem de sen onu programatik olarak tamamlayabilirsin.
Temel Oluşturma Yöntemleri
import java.util.concurrent.CompletableFuture;
public class CompletableFutureBasics {
public static void main(String[] args) throws Exception {
// 1. supplyAsync — değer döndüren asenkron işlem
// ForkJoinPool.commonPool() üzerinde çalışır
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("Çalışan thread: " + Thread.currentThread().getName());
return "Merhaba CompletableFuture!";
});
// 2. runAsync — değer döndürmeyen asenkron işlem (Void)
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
System.out.println("Arka planda log yazılıyor...");
});
// 3. completedFuture — zaten tamamlanmış bir future (test için ideal)
CompletableFuture<String> future3 = CompletableFuture.completedFuture("Hazır değer");
// Sonuçları al
System.out.println(future1.get()); // "Merhaba CompletableFuture!"
System.out.println(future3.get()); // "Hazır değer"
}
}supplyAsync en çok kullanacağın metot. Bir Supplier<T> alır, arka planda çalıştırır ve CompletableFuture<T> döndürür. Varsayılan olarak ForkJoinPool.commonPool() kullanır ama kendi ExecutorService'ini de verebilirsin — birazdan buna geleceğiz.
Zincirleme Operasyonlar: Callback Hell'e Son
CompletableFuture'ın asıl gücü zincirleme (chaining) operasyonlarında. Bunlar üç ana kategoride:
thenApply — Sonucu Dönüştür
thenApply, Stream API'deki map gibi düşün. Bir önceki adımın sonucunu alır, dönüştürür ve yeni bir CompletableFuture döndürür:
CompletableFuture<String> sonuc = CompletableFuture
.supplyAsync(() -> " Merhaba Dünya ")
.thenApply(s -> s.trim()) // boşlukları temizle
.thenApply(s -> s.toUpperCase()) // büyük harfe çevir
.thenApply(s -> s + "!!!"); // sonuna ünlem ekle
System.out.println(sonuc.get()); // "MERHABA DÜNYA!!!"Her thenApply bir öncekinin sonucunu alır ve yeni bir değer üretir. Kod düz bir boru hattı (pipeline) gibi okunur — callback hell yok, iç içe geçmiş yapılar yok.
thenCompose — Asenkron Zincirleme
Peki ya bir adımın sonucuyla başka bir asenkron işlem başlatmak istersen? thenApply kullanırsan CompletableFuture<CompletableFuture<String>> gibi iç içe geçmiş bir tip alırsın. İşte thenCompose bunu düzleştirir — Stream API'deki flatMap gibi:
// Senaryo: Kullanıcı ID'siyle kullanıcıyı bul, sonra siparişlerini getir
public CompletableFuture<String> kullaniciBul(int id) {
return CompletableFuture.supplyAsync(() -> {
// Veritabanı sorgusu simülasyonu
System.out.println("Kullanıcı aranıyor: " + id);
return "Tolgahan"; // kullanıcı adı
});
}
public CompletableFuture<List<String>> siparisleriGetir(String kullaniciAdi) {
return CompletableFuture.supplyAsync(() -> {
// Sipariş servisi çağrısı simülasyonu
System.out.println("Siparişler getiriliyor: " + kullaniciAdi);
return List.of("Laptop", "Mouse", "Klavye");
});
}
// thenCompose ile düz zincirleme — flatMap mantığı
CompletableFuture<List<String>> siparisler = kullaniciBul(42)
.thenCompose(ad -> siparisleriGetir(ad));
System.out.println(siparisler.get()); // [Laptop, Mouse, Klavye]thenApply vs thenCompose farkı: thenApply senkron bir fonksiyon alır (T → U), thenCompose asenkron bir fonksiyon alır (T → CompletableFuture<U>). Eğer zincirin bir sonraki adımı da asenkronsa thenCompose kullan.
thenCombine — İki Bağımsız Sonucu Birleştir
Bazen iki bağımsız asenkron işlemin ikisi de bittiğinde sonuçları birleştirmek istersin:
CompletableFuture<String> isimFuture = CompletableFuture.supplyAsync(() -> {
sleep(200); // simülasyon
return "Tolgahan";
});
CompletableFuture<Integer> yasFuture = CompletableFuture.supplyAsync(() -> {
sleep(300); // simülasyon
return 28;
});
// İkisi de bittiğinde birleştir
CompletableFuture<String> birlesik = isimFuture.thenCombine(yasFuture,
(isim, yas) -> isim + " (" + yas + " yaş)");
System.out.println(birlesik.get()); // "Tolgahan (28 yaş)"
// Toplam süre: ~300ms (paralel çalıştı, en yavaşı kadar bekledi)Hata Yönetimi: exceptionally ve handle
Asenkron kodda hata yönetimi kritik. CompletableFuture bunu fonksiyonel bir şekilde çözüyor.
exceptionally — Sadece Hatayı Yakala
CompletableFuture<String> sonuc = CompletableFuture
.supplyAsync(() -> {
if (true) throw new RuntimeException("API çöktü!");
return "Başarılı";
})
.exceptionally(ex -> {
System.err.println("Hata yakalandı: " + ex.getMessage());
return "Varsayılan değer"; // fallback
});
System.out.println(sonuc.get()); // "Varsayılan değer"exceptionally sadece hata olduğunda çalışır. Bir fallback değer döndürür ve zincir normal devam eder. Try-catch'ten çok daha temiz.
handle — Hem Başarıyı Hem Hatayı Yakala
CompletableFuture<String> sonuc = CompletableFuture
.supplyAsync(() -> {
// Bu bazen başarılı, bazen hatalı olabilir
if (Math.random() > 0.5) throw new RuntimeException("Şanssızlık!");
return "Başarılı sonuç";
})
.handle((deger, hata) -> {
if (hata != null) {
// Hata durumu — loglayıp fallback dön
System.err.println("Hata: " + hata.getMessage());
return "Fallback değer";
}
// Başarı durumu — değeri dönüştür
return deger.toUpperCase();
});
System.out.println(sonuc.get());handle her iki durumda da çalışır. İki parametre alır: değer (başarılıysa) ve hata (varsa). İkisinden biri her zaman null olur.
Birden Fazla Future'ı Yönetme
allOf — Hepsini Bekle
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> { sleep(100); return "A"; });
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> { sleep(200); return "B"; });
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> { sleep(150); return "C"; });
// Üçü de bitene kadar bekle
CompletableFuture<Void> hepsi = CompletableFuture.allOf(f1, f2, f3);
// allOf Void döner, sonuçları elle topla
hepsi.thenRun(() -> {
try {
String sonuclar = f1.get() + ", " + f2.get() + ", " + f3.get();
System.out.println("Tüm sonuçlar: " + sonuclar); // "A, B, C"
} catch (Exception e) {
e.printStackTrace();
}
});allOf hepsinin bitmesini bekler ama direkt sonuç döndürmez (Void döner). Sonuçları her future'dan ayrı ayrı alman gerekir. Bunu daha kullanışlı hale getirmek için bir yardımcı metot yazabilirsin:
// Tüm future'ların sonuçlarını liste olarak toplayan yardımcı metot
@SafeVarargs
public static <T> CompletableFuture<List<T>> allOfResults(
CompletableFuture<T>... futures) {
return CompletableFuture.allOf(futures)
.thenApply(v -> Arrays.stream(futures)
.map(CompletableFuture::join) // join = get ama checked exception fırlatmaz
.collect(Collectors.toList()));
}
// Kullanım
CompletableFuture<List<String>> tumSonuclar = allOfResults(f1, f2, f3);
System.out.println(tumSonuclar.get()); // [A, B, C]anyOf — İlk Biteni Al
CompletableFuture<Object> ilkBiten = CompletableFuture.anyOf(f1, f2, f3);
System.out.println("İlk biten: " + ilkBiten.get()); // Muhtemelen "A" (100ms)anyOf yarış (race) mantığıyla çalışır. İlk tamamlanan future'ın sonucunu döndürür. Birden fazla kaynaktan veri çekip en hızlı yanıtı almak istediğinde kullanışlıdır.
Gerçek Dünya Örneği: Paralel HTTP Çağrıları
Şimdi öğrendiklerimizi birleştirelim. Bir e-ticaret API'sinde ürün detay sayfası için birden fazla servisi paralel çağıran gerçekçi bir örnek:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ProductPageService {
private final HttpClient httpClient;
private final ExecutorService executor;
public ProductPageService() {
this.executor = Executors.newFixedThreadPool(10);
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.executor(executor)
.build();
}
// Tek bir HTTP GET çağrısı yapan yardımcı metot
private CompletableFuture<String> fetchAsync(String url) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(3))
.GET()
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.exceptionally(ex -> {
System.err.println("HTTP hatası [" + url + "]: " + ex.getMessage());
return "{}"; // boş JSON fallback
});
}
// Ürün detay sayfası — tüm verileri paralel çek
public CompletableFuture<ProductPageData> getProductPage(long productId, long userId) {
// 4 bağımsız servis çağrısını aynı anda başlat
CompletableFuture<String> urunFuture = fetchAsync(
"https://api.example.com/products/" + productId);
CompletableFuture<String> stokFuture = fetchAsync(
"https://api.example.com/inventory/" + productId);
CompletableFuture<String> oneriFuture = fetchAsync(
"https://api.example.com/recommendations?user=" + userId);
CompletableFuture<String> yorumFuture = fetchAsync(
"https://api.example.com/reviews?product=" + productId);
// Dördü de bittiğinde sonuçları birleştir
return CompletableFuture.allOf(urunFuture, stokFuture, oneriFuture, yorumFuture)
.thenApply(v -> new ProductPageData(
urunFuture.join(),
stokFuture.join(),
oneriFuture.join(),
yorumFuture.join()
))
.orTimeout(5, java.util.concurrent.TimeUnit.SECONDS) // 5sn timeout
.exceptionally(ex -> {
System.err.println("Sayfa yüklenemedi: " + ex.getMessage());
return ProductPageData.empty();
});
}
record ProductPageData(String product, String stock,
String recommendations, String reviews) {
static ProductPageData empty() {
return new ProductPageData("{}", "{}", "[]", "[]");
}
}
}Bu örnekte birkaç önemli nokta var:
Kendi ExecutorService'imizi verdik —
ForkJoinPool.commonPool()uygulama genelinde paylaşılır. I/O yoğun işlemler (HTTP, veritabanı) için ayrı bir thread pool oluşturmak best practice'tir. Aksi halde common pool'u tıkarsın ve uygulamanın diğer kısımları etkilenir.`exceptionally` ile fallback — Her HTTP çağrısı bağımsız olarak hata yönetimi yapıyor. Stok servisi çökse bile ürün bilgisi gösterilebilir.
`orTimeout` — Java 9 ile gelen bu metot, belirli sürede tamamlanmazsa
TimeoutExceptionfırlatır. Production'da mutlaka timeout koymalısın.`join()` vs `get()` —
join()checked exception fırlatmaz (CompletionExceptionfırlatır), bu yüzden lambda içinde kullanması daha kolay.
Thread Pool Seçimi: Kritik Bir Karar
CompletableFuture'ın varsayılan olarak ForkJoinPool.commonPool() kullanması bir kolaylık ama aynı zamanda bir tuzak. Bu pool, CPU çekirdek sayısı - 1 kadar thread içerir. I/O işlemleri (HTTP çağrısı, veritabanı sorgusu, dosya okuma) thread'i bloklar ve pool'u tıkar.
// ❌ YANLIŞ — I/O işlemleri common pool'da
CompletableFuture.supplyAsync(() -> {
return httpClient.send(request, BodyHandlers.ofString()); // thread bloklanır!
});
// ✅ DOĞRU — I/O işlemleri için özel pool
ExecutorService ioPool = Executors.newFixedThreadPool(20);
CompletableFuture.supplyAsync(() -> {
return httpClient.send(request, BodyHandlers.ofString());
}, ioPool); // ikinci parametre olarak pool'u geçKural: CPU-yoğun işlemler (hesaplama, dönüşüm) için commonPool() uygundur. I/O-yoğun işlemler için her zaman kendi pool'unu oluştur.
Java 21 ile gelen Virtual Thread'ler bu sorunu büyük ölçüde çözer:
// Java 21+ — Virtual thread'ler ile sınırsız I/O paralelliği
ExecutorService virtualPool = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture.supplyAsync(() -> {
return httpClient.send(request, BodyHandlers.ofString());
}, virtualPool);Yaygın Hatalar ve Tuzaklar
1. Future'ın Sonucunu Hiç Tüketmemek
// ❌ Hata sessizce yutulur — kimse fark etmez
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Kritik hata!");
}).thenApply(s -> s.toUpperCase());
// Bu future'ı ne get() ne join() ile çağırırsan, hatayı asla görmezsinSonucu tüketmediğin bir CompletableFuture'daki hatalar sessizce kaybolur. Her zaman ya get()/join() çağır ya da exceptionally/handle ile hatayı yakala.
2. thenApply vs thenApplyAsync Farkını Bilmemek
CompletableFuture.supplyAsync(() -> "merhaba")
.thenApply(s -> s.toUpperCase()) // aynı thread'de çalışır
.thenApplyAsync(s -> s + "!!!"); // farklı thread'de çalışırthenApply bir önceki adımın thread'inde çalışır. thenApplyAsync yeni bir thread'de çalışır. Hafif dönüşümler için thenApply, ağır işlemler veya I/O için thenApplyAsync kullan.
3. get() ile Deadlock Riski
// ❌ Deadlock tehlikesi — aynı pool'da birbirini bekleyen future'lar
ExecutorService pool = Executors.newFixedThreadPool(1); // sadece 1 thread!
CompletableFuture<String> outer = CompletableFuture.supplyAsync(() -> {
// İç future da aynı pool'u kullanmaya çalışıyor ama thread yok!
CompletableFuture<String> inner = CompletableFuture.supplyAsync(() -> "iç", pool);
return inner.join(); // sonsuza kadar bekler — DEADLOCK!
}, pool);Thread pool boyutu yetersizse ve iç içe future'lar aynı pool'u kullanıyorsa deadlock oluşur. Bu yüzden iç içe future'lar için farklı pool'lar kullanmayı düşün.
4. Zincirde İstisna Yönetimini Unutmak
// ❌ exceptionally sadece kendi üstündeki hatayı yakalar
CompletableFuture.supplyAsync(() -> "veri")
.thenApply(s -> {
throw new RuntimeException("Dönüşüm hatası!");
})
.exceptionally(ex -> "fallback") // Bu yakalar ✓
.thenApply(s -> {
throw new RuntimeException("İkinci hata!");
});
// Bu ikinci hata yakalanmaz! ✗
// ✅ Her kritik adımdan sonra hata yönetimi koy veya en sona handle ekle
CompletableFuture.supplyAsync(() -> "veri")
.thenApply(s -> riskliBirIslem(s))
.thenApply(s -> baskaRiskliBirIslem(s))
.handle((sonuc, hata) -> {
// Zincirin herhangi bir yerindeki hatayı yakalar
if (hata != null) return "genel fallback";
return sonuc;
});Best Practices
Timeout her zaman koy.
orTimeout()veyacompleteOnTimeout()kullan. Timeout olmayan asenkron çağrı, production'da bilet kesilir.I/O için özel thread pool kullan.
commonPool()CPU-bound işler için tasarlandı. HTTP, veritabanı, dosya I/O içinExecutors.newFixedThreadPool()veya Java 21'denewVirtualThreadPerTaskExecutor()tercih et.`join()` tercih et, `get()` değil. Lambda içinde checked exception fırlatmayan
join()daha kullanışlıdır.get()sadece timeout ile kullanman gerektiğinde mantıklı:get(5, TimeUnit.SECONDS).Hata yönetimini asla atla. Her zincirin sonunda
exceptionallyveyahandleolmalı. Silent failure (sessiz hata) en tehlikeli bug türüdür.İsimlendirme anlamlı olsun.
cf1,cf2değil,stokFuture,fiyatFuturegibi isimler kullan. Asenkron kod zaten takip edilmesi zordur, isimler yardımcı olsun.Test yazarken `completedFuture` kullan. Mock servislerde gerçek asenkron işlem başlatmak yerine
CompletableFuture.completedFuture(mockDeger)döndür. Testler deterministic ve hızlı olur.Zinciri çok uzatma. 5-6 adımdan uzun bir CompletableFuture zinciri okunması zor hale gelir. Ara adımları anlamlı metotlara çıkar.
Java 9+ ile Gelen Eklemeler
Java 9 ve sonrasında CompletableFuture'a güçlü eklemeler yapıldı:
// Java 9 — Timeout desteği
CompletableFuture<String> future = fetchAsync("https://api.example.com/data")
.orTimeout(3, TimeUnit.SECONDS) // 3sn'de tamamlanmazsa TimeoutException
.completeOnTimeout("varsayılan", 3, TimeUnit.SECONDS); // veya varsayılan değer dön
// Java 9 — copy() ile defensive copy
CompletableFuture<String> copy = future.copy(); // dışarıya read-only referans ver
// Java 12 — exceptionallyCompose, asenkron hata kurtarma
CompletableFuture<String> resilient = fetchAsync("https://primary.api.com/data")
.exceptionallyCompose(ex -> fetchAsync("https://backup.api.com/data")); // fallback servisi çağırexceptionallyCompose özellikle güçlü — hata durumunda başka bir asenkron işlem başlatabilirsin. Circuit breaker, retry, fallback servis gibi pattern'leri temiz bir şekilde uygularsın.
Sonuç
CompletableFuture, Java'da asenkron programlamanın temel taşı. Bu yazıda öğrendiklerini özetleyelim:
`supplyAsync` ile asenkron işlem başlat, `thenApply` ile sonucu dönüştür, `thenCompose` ile asenkron zincirle
`thenCombine` ile bağımsız işlemleri birleştir, `allOf` ile hepsini bekle
`exceptionally` ve `handle` ile hata yönetimini fonksiyonel yap
I/O işlemleri için kendi thread pool'unu oluştur,
commonPool()'a güvenmeTimeout koymayı asla unutma
Java 9+'daki
orTimeout,completeOnTimeout,exceptionallyComposegibi eklemeleri kullan
CompletableFuture öğrenme eğrisi biraz dik olabilir, ama bir kez alıştığında senkron koda geri dönmek istemeyeceksin. Özellikle mikroservis mimarilerinde, birden fazla servisi paralel çağırmak performansı dramatik şekilde artırır. Bir sonraki adım olarak Project Reactor veya Virtual Threads (Java 21) ile tanışmanı öneririm — ama önce CompletableFuture'ı sağlam öğren, çünkü temeli bu oluşturuyor.
Bu yazıyı beğendiniz mi?
Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.
Bu konuyu derinlemesine öğrenmek ister misin?
Java Programlama: Sıfırdan İleri Seviyeye
İlgili Yazılar
Java Optional: NullPointerException'a Kesin Çözüm ve Doğru Kullanım Rehberi
Java Optional sınıfını sıfırdan ileri seviyeye öğrenin. NullPointerException'a kesin çözüm, doğru kullanım kalıpları, an...
Java Nedir? Kapsamlı Başlangıç Rehberi 2026
Java nedir, ne işe yarar, nasıl çalışır? JVM, JDK, JRE farkları, Java ile neler yapılır, kariyer fırsatları ve öğrenme y...
Java Stream API: Koleksiyonları Fonksiyonel Programlama ile Yönetmenin Tam Rehberi
Java Stream API'yi sıfırdan ileri seviyeye kadar öğrenin. Gerçek dünya örnekleri, yaygın hatalar, performans ipuçları ve...