Gereksinimler ve Ön Hazırlık
Bu rehberdeki kod örneklerini çalıştırabilmek için sisteminizde Java 17 veya daha güncel bir sürümün yüklü olması önerilir. Java 8 ile gelen özellikler temel alınsa da, modern sürümlerdeki performans iyileştirmeleri ve hata yönetimi pratikleri baz alınmıştır.
- JDK: Java 17 veya 21 (LTS sürümleri).
- IDE: IntelliJ IDEA, Eclipse veya VS Code.
- Build Aracı: Maven veya Gradle (Proje bağımlılıklarını yönetmek için).
- Temel Bilgi: Java Stream API ve Lambda ifadelerine aşinalık.
CompletableFuture Nedir ve Neden Kullanılır?
CompletableFuture, Java'nın Future arayüzünü genişleten, asenkron programlamayı bir "pipeline" (boru hattı) mantığıyla yönetmemizi sağlayan bir sınıftır. Geleneksel Future yapısı, işlemin sonucunu almak için get() metodunu çağırıp ana iş parçacığını bekletmeye zorlarken, CompletableFuture bize "işlem bittiğinde şunu yap" (callback) deme imkanı sunar.
| Özellik | Geleneksel Future | CompletableFuture |
|---|---|---|
| Bloklama | Evet (get() metodu) | Hayır (Non-blocking) |
| Zincirleme | Desteklenmiyor | Destekleniyor (thenApply, thenCompose) |
| Hata Yönetimi | Zor | Kolay (exceptionally) |
Adım Adım Asenkron Görev Başlatma
Bir görevi asenkron olarak başlatmak için supplyAsync metodu kullanılır. Bu metot, varsayılan olarak ForkJoinPool.commonPool() üzerinde çalışır. Aşağıdaki örnekte, basit bir veri getirme işlemini asenkron olarak nasıl başlatacağımızı görelim.
import java.util.concurrent.CompletableFuture;
public class AsenkronOrnek {
public static void main(String[] args) {
CompletableFuture veriGelecek = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000); // Ağ gecikmesini simüle eder
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Veri başarıyla yüklendi!";
});
System.out.println("Ana iş parçacığı devam ediyor...");
// Sonucu bekle ve yazdır
System.out.println(veriGelecek.join());
}
}
Burada supplyAsync, bir Supplier fonksiyonu alır ve işlemin sonucunu bir CompletableFuture nesnesi içinde döner. join() metodu ise işlem tamamlanana kadar bekler ancak get() metodundan farklı olarak checked exception fırlatmaz.
Görev Zincirleme: thenApply ve thenAccept Kullanımı
Asenkron işlemler genellikle birbirine bağlıdır. Bir veriyi çektikten sonra onu dönüştürmek veya bir veritabanına kaydetmek isteyebilirsiniz. thenApply, bir önceki işlemin sonucunu alıp dönüştürmek için kullanılırken, thenAccept sadece sonucu tüketir.
CompletableFuture.supplyAsync(() -> "Java")
.thenApply(s -> s + " Dünyası")
.thenApply(String::toUpperCase)
.thenAccept(sonuc -> System.out.println("İşlenen veri: " + sonuc));
Bu zincir yapısı sayesinde karmaşık iş akışlarını okunabilir bir şekilde yazabiliriz. Her adım, bir önceki adımın tamamlanmasını bekler ve asenkron olarak çalışmaya devam eder.
Birden Fazla Görevi Birleştirme: thenCompose ve thenCombine
İki bağımsız asenkron işlemi birleştirmek için thenCombine, bir işlemin sonucuna bağlı ikinci bir asenkron işlem başlatmak için ise thenCompose kullanılır. Bu, özellikle mikro hizmet mimarilerinde birden fazla API'den veri toplarken kritik öneme sahiptir.
CompletableFuture isimGelecek = CompletableFuture.supplyAsync(() -> "Ahmet");
CompletableFuture soyisimGelecek = CompletableFuture.supplyAsync(() -> "Yılmaz");
CompletableFuture birlesikGelecek = isimGelecek.thenCombine(soyisimGelecek, (isim, soyisim) -> isim + " " + soyisim);
System.out.println(birlesikGelecek.join());
thenCombine metodu, her iki işlemin sonucunu alıp birleştirerek yeni bir sonuç üretir. Bu yöntem, paralel işlem gücünden yararlanarak toplam süreyi minimize eder.
Hata Yönetimi ve Exceptionally Kullanımı
Asenkron süreçlerde hata yönetimi, ana iş parçacığından farklı bir bağlamda gerçekleştiği için dikkat gerektirir. exceptionally bloğu, zincirleme işlem sırasında oluşabilecek hataları yakalamak ve varsayılan bir değer dönmek için kullanılır.
CompletableFuture sonuc = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("Bağlantı hatası!");
return "Başarılı";
}).exceptionally(ex -> {
System.err.println("Hata yakalandı: " + ex.getMessage());
return "Varsayılan Değer";
});
System.out.println(sonuc.join());
Kritik Güvenlik Uyarısı: Asenkron görevlerde paylaşılan kaynaklara (shared resources) erişirken mutlaka thread-safe (iş parçacığı güvenli) yapılar kullanın. Aksi takdirde "race condition" (yarış durumu) oluşabilir ve verileriniz tutarsız hale gelebilir. Üretim ortamındaForkJoinPoolyerine iş yüküne özelExecutorServicetanımlamanız önerilir.
Sıkça Sorulan Sorular
CompletableFuture ile ExecutorService arasındaki fark nedir?
ExecutorService, görevlerin hangi iş parçacığında çalışacağını yöneten bir havuzdur. CompletableFuture ise bu görevlerin sonuçlarını yöneten ve birbirine bağlayan bir arayüzdür. Genellikle birlikte kullanılırlar.
Neden ForkJoinPool.commonPool() yerine özel Executor kullanmalıyım?
commonPool() tüm uygulama genelinde paylaşılır. Eğer çok uzun süren veya bloklayan bir işlem yaparsanız, uygulamanızdaki diğer tüm asenkron işlemleri yavaşlatabilirsiniz.
CompletableFuture ile veritabanı işlemleri güvenli midir?
Evet, ancak veritabanı bağlantı havuzunuzun (Connection Pool) boyutunu asenkron görev sayısına göre ayarlamanız gerekir. Bağlantı havuzu tükenirse uygulamanız kilitlenebilir.
thenApply ile thenCompose farkı nedir?
thenApply sonucu dönüştürürken, thenCompose bir başka CompletableFuture döndüren fonksiyonları birbirine bağlamak için kullanılır (iç içe geçmiş future'ları düzleştirmek için).
Uygulamam neden asenkron çalışmıyor?
Eğer join() veya get() metodunu zincirin hemen başında çağırırsanız, işlem asenkron değil senkron çalışır. Zinciri kurduktan sonra en sonda bekleme yapmalısınız.
CompletableFuture ile Birim Testi (Unit Testing) Stratejileri
Asenkron kodların test edilmesi, geleneksel senkron kodlara göre daha karmaşıktır. İşlemler arka planda gerçekleştiği için, test metodunuz asenkron görev tamamlanmadan sonlanabilir. Bu durum, testlerin hatalı bir şekilde "başarılı" dönmesine veya "false negative" sonuçlar üretmesine neden olur.
Test süreçlerinde CompletableFuture ile çalışırken join() veya get() metodlarını kullanarak thread'in tamamlanmasını beklemek en yaygın yaklaşımdır. Ancak, sonsuz döngüleri engellemek için mutlaka zaman aşımı (timeout) parametresi eklenmelidir.
@Test
public void testAsenkronIslem() throws Exception {
CompletableFuture future = CompletableFuture.supplyAsync(() -> "Veri Hazır");
// Timeout ile bekleme yaparak testin kilitlenmesini önleriz
String sonuc = future.get(2, TimeUnit.SECONDS);
assertEquals("Veri Hazır", sonuc);
}
Daha karmaşık senaryolarda, Awaitility gibi kütüphaneler kullanarak asenkron işlemlerin belirli bir süre içerisinde gerçekleşip gerçekleşmediğini daha okunabilir bir şekilde doğrulayabilirsiniz.
İleri Düzey Performans Optimizasyonu ve İzleme
CompletableFuture kullanırken yapılan en büyük hata, tüm görevler için varsayılan ForkJoinPool.commonPool() havuzunu kullanmaktır. Özellikle G/Ç (I/O) ağırlıklı işlemlerde, havuzun kısıtlı olması tüm uygulamanın performansını düşürür.
Özel Thread Havuzu Yapılandırması
İşlem türüne göre (CPU yoğun veya I/O yoğun) ayrı thread havuzları tanımlamak, sistem kaynaklarını daha verimli kullanmanızı sağlar. Aşağıdaki örnekte, veritabanı işlemleri için özel bir thread havuzu oluşturulmuştur:
// Veritabanı işlemleri için özel havuz
ExecutorService dbExecutor = Executors.newFixedThreadPool(10);
CompletableFuture.supplyAsync(() -> {
// Veritabanı sorgusu
return "DB Sonucu";
}, dbExecutor).thenAccept(System.out::println);
İzleme (Monitoring) İpuçları
Asenkron görevlerin ne kadar sürede tamamlandığını veya havuzda kaç görevin beklediğini izlemek için Micrometer veya Prometheus gibi araçlar entegre edilmelidir. Ayrıca, görevlerin isimlerini belirlemek hata ayıklama sürecini büyük ölçüde kolaylaştırır.
Profesyonel İpucu: Thread havuzlarınıza isim verirkenThreadFactorykullanarak anlamlı isimler atayın. Bu sayedejstackveyaVisualVMile thread dump aldığınızda hangi görevin hangi havuzda çalıştığını kolayca görebilirsiniz.
| İşlem Tipi | Önerilen Havuz Yapısı |
|---|---|
| CPU Yoğun | İşlemci çekirdek sayısı kadar thread (N+1) |
| I/O Yoğun | Daha geniş thread havuzu (Örn: 50-100 thread) |
Sonuç
Java ile CompletableFuture kullanarak asenkron görev yönetimi yapmak, uygulamanızın ölçeklenebilirliğini doğrudan artırır. Bu rehberde, temel başlatmadan karmaşık zincirleme yöntemlerine ve hata yönetimine kadar geniş bir perspektif sunduk. Bir sonraki adım olarak, projenizde CompletableFuture ile Virtual Threads (Sanal İş Parçacıkları - Java 21+) entegrasyonunu inceleyerek performansı daha da üst seviyeye taşıyabilirsiniz.
Sorumluluk Reddi: Bu makalede paylaşılan kod örnekleri eğitim amaçlıdır. Üretim ortamında kullanılacak kodlarda, veritabanı bağlantı yönetimi, hata loglama ve thread havuzu yapılandırması gibi kurumsal standartların uygulanması geliştiricinin sorumluluğundadır.


Yorumlar (0)
Yorum Yaz