Java 21 Sanal İş Parçacıkları (Virtual Threads): Yüksek Eşzamanlılığa Sade Yol
Java 21 ile gelen sanal iş parçacıkları (Project Loom) nedir, platform thread'lerinden farkı nedir ve thread-per-request modelini nasıl yeniden ucuzlatır? Kod örnekleri, tuzaklar ve ölçüm ipuçlarıyla.

Sanal iş parçacıkları (virtual threads), Java 21 ile kararlı hale gelen ve yıllardır Project Loom adıyla geliştirilen bir özellik. Tek cümleyle: artık milyonlarca eşzamanlı görevi, klasik thread-per-request modelinin sadeliğiyle ama platform thread’lerinin maliyeti olmadan çalıştırabiliyoruz.
Bu yazıda sanal iş parçacıklarının ne olduğunu, neden önemli olduğunu ve gerçek kodda nasıl kullanıldığını; ayrıca düşmeden önce bilmen gereken tuzakları anlatıyorum. Daha fazla Java içeriği için Java sayfama göz atabilirsin.
Sorun: platform thread’leri pahalı
Klasik bir Java thread’i (platform thread), işletim sisteminin bir thread’ine bire bir karşılık gelir. Her biri ~1 MB yığın (stack) ayırır ve OS tarafından zamanlanır. Bir web sunucusunda “her istek için bir thread” açmak isterdik çünkü kod basit ve okunabilir olur — ama birkaç bin thread’den sonra bellek ve bağlam değiştirme (context switch) maliyeti seni durdurur.
Bu yüzden yıllarca reaktif ve asenkron API’lere kaçtık: CompletableFuture, callback zincirleri, reaktif akışlar… Performanslı ama okuması ve hata ayıklaması zor kod.
Sanal iş parçacığı nedir?
Sanal iş parçacığı, JVM tarafından zamanlanan hafif bir thread’tir. Bir OS thread’ine kalıcı olarak bağlı değildir; yalnızca gerçekten iş yaptığı (CPU kullandığı) anlarda bir taşıyıcı (carrier) platform thread’ine bağlanır. Bir G/Ç çağrısında (ağ, dosya, veritabanı) beklemeye girdiğinde JVM onu taşıyıcıdan ayırır (unmount) ve taşıyıcıyı başka bir sanal thread’e verir.
Sonuç: on binlerce sanal thread, avuç dolusu OS thread’i üzerinde çalışabilir.
flowchart TD
subgraph JVM
V1[Sanal thread 1] --> C1[Taşıyıcı thread]
V2[Sanal thread 2] --> C1
V3[Sanal thread 3] --> C2[Taşıyıcı thread]
V4[Sanal thread N] -.G/Ç'de bekliyor.-> P[(Park edildi)]
end
C1 --> OS[(OS thread havuzu)]
C2 --> OSNasıl oluşturulur?
En basit haliyle bir sanal thread başlatmak:
Thread.startVirtualThread(() -> { System.out.println("Merhaba, sanal dünya!");});Gerçek hayatta ise görevleri bir ExecutorService ile yönetirsin. Buradaki kilit nokta: her görev için yeni bir sanal thread açan executor’ı kullanmak.
import java.util.concurrent.Executors;
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 10_000; i++) { int taskId = i; executor.submit(() -> { // G/Ç ağırlıklı iş — örn. bir HTTP çağrısı var result = callRemoteService(taskId); process(result); }); }} // executor.close() tüm görevlerin bitmesini beklerYukarıdaki 10.000 görev, 10.000 OS thread’i açmadan çalışır. Kod ise tamamen senkron ve okunabilir.
Adım adım: Spring Boot’ta açmak
Spring Boot 3.2+ ile sanal iş parçacıklarını tek satırlık bir ayarla etkinleştirebilirsin:
Java 21+ kullandığından emin ol (
java -version).application.propertiesdosyasına şu satırı ekle:application.properties spring.threads.virtual.enabled=trueUygulamayı başlat. Artık Tomcat, her HTTP isteğini bir sanal thread üzerinde işler — kodunda hiçbir değişiklik yapmadan.
Ne zaman kullanmalı, ne zaman kaçınmalı?
Artılar
- + G/Ç ağırlıklı, çok sayıda eşzamanlı görev (web sunucuları, API ağ geçitleri)
- + Senkron, okunabilir kod — asenkron karmaşıklığı yok
- + Mevcut blocking API’lerle (JDBC, HttpClient) uyumlu
- + Çok ucuz: on binlerce thread sorun değil
Eksiler
- − CPU-yoğun işlerde fayda sağlamaz (klasik havuz daha iyi)
- − Uzun synchronized blokları pinning’e yol açabilir
- − ThreadLocal’ın aşırı kullanımı bellek baskısı yaratır
- − Profilleme/araçlar hâlâ olgunlaşıyor
Dikkat: “pinning” tuzağı
Sanal bir thread, synchronized bir blok içindeyken bir G/Ç çağrısında bloklanırsa, taşıyıcı thread’ine sabitlenir (pinned) ve onu serbest bırakamaz. Çok sayıda pinning, sanal thread’lerin tüm avantajını yok edebilir.
synchronized (lock) { // ❌ Kilit altında uzun G/Ç → pinning var data = database.query(sql); cache.put(key, data);}private final ReentrantLock lock = new ReentrantLock();
// ✓ ReentrantLock pinning'e yol açmazlock.lock();try { var data = database.query(sql); cache.put(key, data);} finally { lock.unlock();}Sonuç
Sanal iş parçacıkları, Java’da eşzamanlılığı yıllar sonra yeniden basit yapıyor: senkron kod yaz, yine de yüksek ölçeklen. Anahtar kurallar:
- Görev başına yeni sanal thread aç, havuzlama.
- G/Ç ağırlıklı işlerde kullan; CPU-yoğun işlerde klasik havuzlarda kal.
synchronized+ uzun G/Ç kombinasyonundan kaçın,ReentrantLockkullan.- Her zaman ölç.
Daha fazlası için #java ve #concurrency etiketlerine, ya da derli toplu Java sayfasına bakabilirsin.
Sıkça sorulan sorular
Sanal iş parçacıkları platform thread'lerinin yerini tamamen alır mı?
Hayır. Sanal iş parçacıkları G/Ç ağırlıklı, çok sayıda bekleyen görev için idealdir. CPU-yoğun işlerde klasik platform thread'leri ve havuzlar hâlâ doğru seçimdir.
Sanal iş parçacıkları için havuz (thread pool) kurmalı mıyım?
Hayır. Sanal thread'ler ucuzdur; havuzlamak anti-pattern'dir. Her görev için yeni bir sanal thread aç (Executors.newVirtualThreadPerTaskExecutor).
synchronized blokları sorun çıkarır mı?
Java 21'de uzun süreli G/Ç içeren synchronized blokları "pinning"e yol açıp taşıyıcı thread'i bloklayabilir. Kritik yollarda ReentrantLock tercih et; sonraki sürümler pinning'i azaltır.
Hangi Java sürümü gerekli?
Sanal iş parçacıkları Java 21'de kararlı (LTS) hale geldi. Java 19/20'de önizleme olarak vardı.