Senkron vs Asenkron Programlama
Giriş
Bir kafede kahve sipariş verdiğinizi düşünün. Senkron model: Kasada sipariş veriyorsunuz, barista kahvenizi hazırlıyor, siz kasanın önünde dikiliyor, kahve hazır olunca alıp gidiyorsunuz. Arkanızdaki 20 kişi? Onlar da bekliyor. Asenkron model: Sipariş veriyorsunuz, bir numara alıyorsunuz, kenara çekiliyorsunuz. Barista kahvenizi hazırlarken kasadaki diğer müşteriler de sipariş verebiliyor. Numaranız çağrılınca kahvenizi alıyorsunuz.
Modern yazılım geliştirmede performans ve kullanıcı deneyimi en kritik metriklerin başında gelir. Bir e-ticaret uygulaması düşünün: kullanıcı sipariş verdiğinde ödeme işlemi, stok kontrolü, e-posta gönderimi, fatura oluşturma ve bildirim gönderme gibi birçok adım gerçekleşir. Bu adımları sırayla (senkron) çalıştırmak kullanıcıyı 7-10 saniye bekletebilir. Oysa bazı adımlar birbirinden bağımsızdır ve paralel çalışabilir — işte asenkron programlama bu noktada devreye girer.
Bu derste senkron ve asenkron programlama modellerini, blocking ve non-blocking I/O kavramlarını, thread modeli ve kaynak kullanımını, hangi senaryoda hangi modelin uygun olduğunu ve Spring Boot'un sunduğu asenkron araçları derinlemesine öğreneceğiz.
Senkron (Blocking) Model
Senkron programlamada her işlem sırayla gerçekleşir. Bir işlem tamamlanmadan diğerine geçilmez. Çalışan thread, işlem bitene kadar bloke olur ve başka hiçbir iş yapamaz. Buna blocking model denir.
Sipariş İşleme — Senkron Yaklaşım
@Service
public class OrderService {
public OrderResult processOrder(Order order) {
// Adım 1: Ödeme al (2 saniye)
PaymentResult payment = paymentService.charge(order);
// Adım 2: Stok ayır (1 saniye)
InventoryResult stock = inventoryService.reserve(order);
// Adım 3: Onay e-postası gönder (3 saniye)
EmailResult email = emailService.sendConfirmation(order);
// Adım 4: Fatura oluştur (1 saniye)
InvoiceResult invoice = invoiceService.generate(order);
// Toplam bekleme: 2 + 1 + 3 + 1 = 7 saniye!
// Kullanıcı 7 saniye boyunca "loading" görüyor
return new OrderResult(payment, stock, email, invoice);
}
}Thread zaman çizelgesi:
Thread-1: [===PAYMENT(2s)===][===STOCK(1s)===][===EMAIL(3s)===][===INVOICE(1s)===]
0 2 3 6 7 saniye
↑ YanıtSenkron Modelin Avantajları
Senkron modelin basitliği en büyük gücüdür:
Anlaşılması kolay: Kod yukarıdan aşağıya sırayla okunur, akış nettir
Hata yönetimi basit: try-catch ile tüm hataları doğrusal olarak yakalarsınız
Debug kolay: Stack trace tam ve anlamlıdır
Transaction bütünlüğü: Tüm adımlar aynı thread'de, sıralı çalışır
Senkron Modelin Dezavantajları
Ancak ciddi sınırlamaları vardır:
Yavaş: Toplam süre = tüm adımların süreleri toplamı
Thread israfı: Thread, I/O beklerken CPU kullanmaz ama bellekte yer kaplar
Ölçeklenemezlik: Her eşzamanlı istek bir thread gerektirir, yüksek trafikte thread havuzu tükenir
Domino etkisi: Bir adım yavaşlarsa tüm işlem yavaşlar
Thread Modeli ve Kaynak Tüketimi
Java'da her thread belirli miktarda sistem kaynağı tüketir. Bu kavramı anlamak, senkron modelin neden ölçeklenmediğini kavramak için kritiktir.
Thread Bellek Maliyeti
Her Java thread'i:
- Stack memory: 512 KB – 1 MB (varsayılan: -Xss512k)
- OS thread: kernel yapıları için ek ~8-16 KB
- Thread-local değişkenler: değişken
200 eşzamanlı istek → 200 thread:
- Minimum bellek: 200 × 512 KB = 100 MB (sadece stack!)
- Bu thread'lerin çoğu I/O beklerken HİÇBİR İŞ yapmıyor
1000 eşzamanlı istek → 1000 thread:
- Minimum bellek: 1000 × 512 KB = 500 MB
- Context switching overhead: CPU zamanının %20-30'u boşa giderThread Starvation (Açlığı)
Spring Boot'un varsayılan Tomcat thread pool'u 200 thread içerir. Her istek bir thread kullanır ve işlem süresince tutulur:
Senaryo: Her istek 7 saniye sürüyor (senkron sipariş işleme)
Thread kapasitesi: 200 thread
Her thread'in işleme süresi: 7 saniye
Throughput: 200 / 7 = ~28 istek/saniye
201. istek geldiğinde: Thread yok! → Bekleme kuyruğuna girer
Kuyruk da dolarsa: "Connection refused" → Kullanıcı hata görür
Black Friday'de 500 eşzamanlı istek:
200 istek işleniyor, 300 istek bekliyor
Ortalama bekleme süresi: 7+ saniye
Bazı kullanıcılar: timeout → Sepeti terk ediyor → Gelir kaybı!Asenkron (Non-Blocking) Model
Asenkron programlamada bir işlem başlatıldığında thread bloke olmaz. İşlem arka planda devam ederken thread başka işler yapabilir. İşlem tamamlandığında bir callback, Future veya event mekanizması ile sonuç alınır.
Sipariş İşleme — Asenkron Yaklaşım
@Service
public class AsyncOrderService {
public CompletableFuture<OrderResult> processOrderAsync(Order order) {
// Tüm işlemler AYNI ANDA başlatılır
CompletableFuture<PaymentResult> paymentFuture =
CompletableFuture.supplyAsync(() -> paymentService.charge(order));
CompletableFuture<InventoryResult> stockFuture =
CompletableFuture.supplyAsync(() -> inventoryService.reserve(order));
CompletableFuture<EmailResult> emailFuture =
CompletableFuture.supplyAsync(() -> emailService.sendConfirmation(order));
CompletableFuture<InvoiceResult> invoiceFuture =
CompletableFuture.supplyAsync(() -> invoiceService.generate(order));
// Tüm işlemler paralel çalışır
// Toplam süre: max(2, 1, 3, 1) = 3 saniye (en yavaş işlem kadar)
return CompletableFuture.allOf(
paymentFuture, stockFuture, emailFuture, invoiceFuture)
.thenApply(v -> new OrderResult(
paymentFuture.join(),
stockFuture.join(),
emailFuture.join(),
invoiceFuture.join()
));
}
}Thread zaman çizelgesi:
Thread-1: [===PAYMENT(2s)===]
Thread-2: [=STOCK(1s)=]
Thread-3: [=====EMAIL(3s)=====]
Thread-4: [=INVOICE(1s)=]
0 3 saniye
↑ Yanıt (en yavaş işlem kadar: 3 saniye)Senkron: 7 saniye → Asenkron: 3 saniye → %57 daha hızlı!
Hibrit Yaklaşım: Kritik + Fire-and-Forget
Gerçek dünyada tüm işlemleri paralel yapmak her zaman doğru değildir. Bazı işlemler sıralı olmalıdır (önce ödeme, sonra stok), bazıları ise fire-and-forget (e-posta, bildirim):
@Service
public class SmartOrderService {
private final NotificationService notificationService;
private final ApplicationEventPublisher eventPublisher;
public OrderResult processOrder(Order order) {
// 1. KRİTİK — Sıralı, senkron (ödeme başarılı olmalı ki devam edelim)
PaymentResult payment = paymentService.charge(order);
// 2. KRİTİK — Sıralı, senkron (stok yoksa sipariş oluşturulamaz)
InventoryResult stock = inventoryService.reserve(order);
// 3. Sipariş kaydı (senkron — DB transaction)
Order savedOrder = orderRepository.save(order);
// 4. YAN ETKİLER — Asenkron, fire-and-forget
// Bu işlemler arka planda çalışır, kullanıcıyı BEKLETMEZ
notificationService.sendOrderConfirmation(savedOrder); // @Async
notificationService.notifyWarehouse(savedOrder); // @Async
eventPublisher.publishEvent(new OrderCreatedEvent(savedOrder)); // Event
// Kullanıcıya hemen yanıt dön (2 + 1 = 3 saniye)
// E-posta, bildirim, analitik arka planda devam eder
return new OrderResult(payment, stock, savedOrder.getId());
}
}Blocking vs Non-Blocking I/O
Blocking I/O
Thread, I/O işlemi (disk okuma, ağ isteği, veritabanı sorgusu) tamamlanana kadar bekler. Bu sürede thread CPU kullanmaz ama bellekte yer kaplar ve başka iş yapamaz.
Blocking I/O — Thread yaşam döngüsü:
Thread: [CPU çalışıyor][----I/O BEKLİYOR----][CPU çalışıyor][----I/O BEKLİYOR----]
~%5 CPU ~%95 bekleme ~%5 CPU ~%95 bekleme
Tipik bir web uygulamasında:
- CPU çalışma: %5-10 (JSON parse, iş mantığı, serialization)
- I/O bekleme: %90-95 (veritabanı, HTTP çağrısı, dosya okuma)
Thread'in zamanının %95'i HİÇBİR ŞEY YAPMADAN geçiyor!Non-Blocking I/O
Thread, I/O isteğini gönderir ve hemen geri döner. İşlem tamamlandığında bir bildirim (callback, event) alır. Java NIO (New I/O) paketi bu modeli destekler.
Non-Blocking I/O — Thread yaşam döngüsü:
Thread: [İstek-1 gönder][İstek-2 gönder][İstek-3 gönder][İstek-1 yanıt][İstek-2 yanıt]...
Tek thread, birçok I/O işlemini aynı anda yönetebilir!
Bekleme yok → Thread sürekli çalışıyor → Kaynak verimliliğiKarşılaştırma Tablosu
| Özellik | Blocking I/O | Non-Blocking I/O |
|---|---|---|
| Thread kullanımı | İşlem başına bir thread | Az sayıda thread ile çok işlem |
| Bellek kullanımı | Yüksek (her thread ~512KB-1MB) | Düşük |
| Kodun karmaşıklığı | Basit, sıralı | Callback/Future ile daha karmaşık |
| Ölçeklenebilirlik | Sınırlı (thread sayısı) | Yüksek |
| Hata ayıklama | Kolay (stack trace net) | Zor (asenkron stack trace parçalı) |
| Throughput (yüksek yük) | Düşük | Yüksek |
Thread Pool Kavramı
Asenkron programlamada thread'leri doğrudan oluşturmak yerine thread pool (havuz) kullanılır. Thread oluşturma maliyetlidir (OS thread, stack allocation); pool ile thread'ler yeniden kullanılır.
// ❌ YANLIŞ — Her işlem için yeni thread (kaynak israfı)
public void processAsync() {
new Thread(() -> doWork()).start(); // Her çağrıda yeni thread!
new Thread(() -> doWork()).start(); // Kontrolsüz thread oluşturma
new Thread(() -> doWork()).start(); // Bellek patlar, OS sınırına ulaşılır
}
// ✅ DOĞRU — Thread pool kullanımı
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // Her zaman hazır 5 thread
executor.setMaxPoolSize(20); // Yoğun dönemde 20'ye kadar
executor.setQueueCapacity(100); // 100 görev kuyruğa alınabilir
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}Thread pool mekanizması:
Görev geldi
│
▼
[Core thread boş?] ── Evet ──→ Core thread çalıştırır
│ Hayır
▼
[Kuyrukta yer var?] ── Evet ──→ Kuyruğa ekle, sırasını bekle
│ Hayır
▼
[Max thread'e ulaşıldı?] ── Hayır ──→ Yeni thread oluştur
│ Evet
▼
Rejection Policy (hata fırlat / çağıranı yavaşlat / görevi at)Ne Zaman Asenkron Kullanmalı?
Asenkron programlama her senaryoda doğru seçim değildir. Yanlış yerde kullanmak kodu karmaşıklaştırır ve hata ayıklamayı zorlaştırır. İşte karar vermenizi sağlayacak rehber:
✅ Asenkron Kullanın
1. Bağımsız I/O işlemleri paralel çalışabiliyorsa:
→ E-posta + push bildirim + SMS aynı anda gönderilebilir
→ 3 farklı servisten veri aynı anda çekilebilir
2. Uzun süren işlemler kullanıcıyı bekletmemeli:
→ Rapor oluşturma: "Rapor hazırlanıyor, hazır olunca e-posta atacağız"
→ Dosya işleme: Video transcode, resim optimize
3. Fire-and-forget senaryoları:
→ Audit log yazma
→ Analitik event gönderme
→ Cache invalidation
4. Yüksek eşzamanlılık (high concurrency):
→ 10.000+ eşzamanlı kullanıcı
→ WebSocket bağlantıları
→ Real-time notification❌ Senkron Kalın
1. İşlemler arasında sıralı bağımlılık varsa:
→ Önce ödeme al → sonra stok ayır → sonra kargo başlat
→ Her adım bir öncekinin SONUCUNA bağlı
2. CPU-bound (yoğun hesaplama) işlemleri:
→ Asenkron, I/O bekleme sürelerini optimize eder
→ CPU işlemleri zaten thread'i meşgul tutar
→ CPU-bound için parallelStream() veya ForkJoinPool tercih edin
3. Basitlik kritikse ve performans yeterliyse:
→ 100ms'de biten bir istek için asenkron karmaşıklık gereksiz
→ "Premature optimization is the root of all evil" — Knuth
4. Transaction bütünlüğü gerekiyorsa:
→ Tek veritabanı transaction'ı senkron çalışmalı
→ @Transactional + @Async birlikte DİKKATLİ kullanılmalı
→ Asenkron metot kendi transaction'ını açar
5. Hata yönetimi kritikse:
→ Senkron: try-catch basit ve güvenilir
→ Asenkron: Exception'lar kaybolabilir, CompletableFuture chain'leri karmaşıkKarar Matrisi
Bağımsız mı?
/ \
Evet Hayır
/ \
I/O ağırlıklı? → SENKRON
/ \
Evet Hayır (CPU)
| |
ASENKRON parallelStream
(CompletableFuture, (veya ForkJoinPool)
@Async, Event)Spring Boot'ta Asenkron Araçlar
Spring Boot, asenkron programlama için birden fazla seviyede araç sunar. Her biri farklı senaryolar için idealdir:
1. @Async Anotasyonu
En basit yöntem — metodu ayrı bir thread'de çalıştırır:
@Service
public class NotificationService {
@Async
public void sendEmail(String to, String subject) {
// Bu metot ayrı thread'de çalışır
emailClient.send(to, subject);
}
@Async
public CompletableFuture<Report> generateReport(String type) {
Report report = reportGenerator.create(type);
return CompletableFuture.completedFuture(report);
}
}Kullanım: Fire-and-forget görevler, basit paralel çalıştırma.
2. CompletableFuture
Java 8+ ile gelen güçlü asenkron API — zincirleme, birleştirme, hata yönetimi:
CompletableFuture.supplyAsync(() -> fetchUser(userId))
.thenCompose(user -> fetchOrders(user.getId()))
.thenApply(orders -> calculateTotal(orders))
.exceptionally(ex -> {
log.error("Hata", ex);
return BigDecimal.ZERO;
});Kullanım: Birbirine bağlı asenkron işlem zincirleri, birden fazla kaynağı birleştirme.
3. ThreadPoolTaskExecutor
Thread pool yönetimi ve konfigürasyonu:
@Bean
public TaskExecutor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("email-");
return executor;
}Kullanım: Farklı iş yükleri için ayrı thread pool'lar.
4. @Scheduled
Periyodik görevler — cron, fixedRate, fixedDelay:
@Scheduled(cron = "0 0 2 * * *") // Her gece 02:00
public void cleanupExpiredSessions() {
sessionRepository.deleteExpired();
}Kullanım: Zamanlanmış görevler — temizlik, rapor, senkronizasyon.
5. ApplicationEvent
Olay tabanlı gevşek bağlı (loosely coupled) iletişim:
// Olay yayınla
eventPublisher.publishEvent(new OrderCreatedEvent(order));
// Olay dinle — başka bir sınıfta
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
analyticsService.trackOrder(event.getOrder());
}Kullanım: Modüler mimari, yan etkilerin (e-posta, bildirim, loglama) ana iş mantığından ayrılması.
Performans Karşılaştırması
Aynı senaryoyu senkron ve asenkron modellerle karşılaştıralım:
Senaryo: E-ticaret uygulaması, 1000 eşzamanlı kullanıcı
Her sipariş: ödeme(2s) + stok(1s) + e-posta(3s) + fatura(1s)
SENKRON MODEL:
İstek süresi: 7 saniye
Thread pool: 200 thread
Throughput: 200 / 7 = ~28 istek/saniye
1000 kullanıcı: 1000 / 28 = ~36 saniye (son kullanıcı bu kadar bekler)
ASENKRON MODEL (paralel):
İstek süresi: 3 saniye (en yavaş adım)
Thread pool: 200 thread
Throughput: 200 / 3 = ~67 istek/saniye
1000 kullanıcı: 1000 / 67 = ~15 saniye
HİBRİT MODEL (kritik senkron + fire-and-forget):
İstek süresi: 3 saniye (ödeme + stok) — e-posta/fatura arka planda
Thread pool: 200 thread
Throughput: 200 / 3 = ~67 istek/saniye
Kullanıcı deneyimi: 3 saniye bekleme (en iyi!)Yaygın Hatalar
1. Her Şeyi Asenkron Yapmak
// ❌ YANLIŞ — Gereksiz karmaşıklık
@Async
public CompletableFuture<User> findById(Long id) {
return CompletableFuture.completedFuture(
userRepository.findById(id).orElseThrow()
);
// 1ms'lik bir sorgu için asenkron yapmanın anlamı yok!
}
// ✅ DOĞRU — Basit tutun
public User findById(Long id) {
return userRepository.findById(id).orElseThrow();
}2. Sıralı Bağımlı İşlemleri Paralel Yapmaya Çalışmak
// ❌ YANLIŞ — Ödeme başarılı olmadan stok ayırmamalısınız
CompletableFuture<PaymentResult> payment = payAsync(order);
CompletableFuture<InventoryResult> stock = reserveAsync(order); // Ödeme daha bitmedi!
// Ödeme başarısız olursa stok boşuna ayrılmış olur
// ✅ DOĞRU — Önce ödeme, sonra paralel yan etkiler
PaymentResult payment = paymentService.charge(order); // Senkron — bekle!
if (payment.isSuccessful()) {
CompletableFuture.allOf(
reserveStockAsync(order),
sendEmailAsync(order),
createInvoiceAsync(order)
);
}3. Exception'ları Kaybetmek
// ❌ YANLIŞ — void @Async'te exception sessizce kaybolur
@Async
public void sendEmail(String to, String body) {
emailClient.send(to, body); // Exception fırlatırsa KİMSE BİLMEZ
}
// ✅ DOĞRU — Exception handler tanımlayın
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, params) -> {
log.error("Async exception in {}: {}",
method.getName(), throwable.getMessage(), throwable);
// Alert gönder, metrik kaydet
};
}
}Gerçek Dünya Örneği: Dashboard Aggregation
Bir kullanıcı dashboard'u düşünün. Profil bilgisi, siparişler, cüzdan bakiyesi ve bildirimler ayrı servislerden gelir:
@RestController
@RequiredArgsConstructor
public class DashboardController {
private final UserService userService;
private final OrderService orderService;
private final WalletService walletService;
private final NotificationService notificationService;
// ❌ SENKRON — Her servis sırayla çağrılır: 200+150+100+80 = 530ms
@GetMapping("/dashboard/sync/{userId}")
public DashboardResponse getDashboardSync(@PathVariable Long userId) {
UserProfile profile = userService.getProfile(userId); // 200ms
List<Order> orders = orderService.getRecent(userId); // 150ms
WalletInfo wallet = walletService.getBalance(userId); // 100ms
List<Notification> notifs = notificationService.getUnread(userId); // 80ms
return new DashboardResponse(profile, orders, wallet, notifs);
}
// ✅ ASENKRON — Paralel çağrı: max(200,150,100,80) = 200ms
@GetMapping("/dashboard/async/{userId}")
public DashboardResponse getDashboardAsync(@PathVariable Long userId) {
CompletableFuture<UserProfile> profileFuture =
CompletableFuture.supplyAsync(() -> userService.getProfile(userId));
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(() -> orderService.getRecent(userId));
CompletableFuture<WalletInfo> walletFuture =
CompletableFuture.supplyAsync(() -> walletService.getBalance(userId));
CompletableFuture<List<Notification>> notifsFuture =
CompletableFuture.supplyAsync(() -> notificationService.getUnread(userId));
CompletableFuture.allOf(profileFuture, ordersFuture, walletFuture, notifsFuture)
.join();
return new DashboardResponse(
profileFuture.join(),
ordersFuture.join(),
walletFuture.join(),
notifsFuture.join()
);
}
// ✅✅ ASENKRON + HATA YÖNETİMİ — Bir servis başarısız olursa diğerleri etkilenmez
@GetMapping("/dashboard/resilient/{userId}")
public DashboardResponse getDashboardResilient(@PathVariable Long userId) {
CompletableFuture<UserProfile> profileFuture =
CompletableFuture.supplyAsync(() -> userService.getProfile(userId))
.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> UserProfile.defaultProfile(userId));
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(() -> orderService.getRecent(userId))
.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> List.of());
CompletableFuture<WalletInfo> walletFuture =
CompletableFuture.supplyAsync(() -> walletService.getBalance(userId))
.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> WalletInfo.unavailable());
CompletableFuture<List<Notification>> notifsFuture =
CompletableFuture.supplyAsync(() -> notificationService.getUnread(userId))
.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> List.of());
CompletableFuture.allOf(profileFuture, ordersFuture, walletFuture, notifsFuture)
.join();
return new DashboardResponse(
profileFuture.join(),
ordersFuture.join(),
walletFuture.join(),
notifsFuture.join()
);
}
}Sonuç: 530ms → 200ms → %62 hızlanma, üstelik bir servis çökse bile dashboard kısmen gösterilir.
Özet
Senkron programlama basit, anlaşılır ve hata ayıklaması kolaydır. İşlemler sırayla çalışır, toplam süre tüm adımların toplamıdır. Thread, I/O beklerken hiçbir iş yapmaz.
Asenkron programlama, bağımsız işlemleri paralel çalıştırarak toplam süreyi en yavaş işlem kadar düşürür. Thread'ler verimli kullanılır, yüksek eşzamanlılık sağlanır.
Her şeyi asenkron yapmayın — sıralı bağımlılık varsa senkron kalın. CPU-bound işlemler için parallelStream tercih edin. Basit, hızlı işlemler için asenkron karmaşıklık gereksizdir.
Hibrit yaklaşım çoğu zaman en iyisidir: kritik iş mantığı senkron, yan etkiler (e-posta, bildirim, loglama) asenkron.
Spring Boot 5 temel asenkron araç sunar:
@Async,CompletableFuture,ThreadPoolTaskExecutor,@Scheduled,ApplicationEvent. Her biri farklı senaryolar için idealdir.Asenkron kodda exception handling kritiktir — void metotlarda exception sessizce kaybolur.
AsyncUncaughtExceptionHandlerveyaCompletableFuture.exceptionally()kullanın.
AI Asistan
Sorularını yanıtlamaya hazır