Virtual Threads Nedir?
Java 21 ile birlikte gelen virtual threads, JDK'nın eşzamanlılık modeline eklediği yeni bir thread türüdür. Amaç, yüksek sayıda eşzamanlı I/O işlemini, işletim sistemi thread'lerini tüketmeden yönetebilmektir.
Klasik platform thread'lerinde her Thread nesnesi bir işletim sistemi thread'ine birebir bağlıdır. Bu thread'ler pahalıdır: oluşturma, context switch ve bellek maliyeti nedeniyle pratikte binlerle sınırlı kalırsınız. Bir istek bloklandığında (veritabanı çağrısı, HTTP isteği, dosya okuma), o thread boşta bekler.
Virtual threads bu denklemi değiştirir. JVM içinde hafif sanal thread'ler oluşturulur ve bunlar az sayıda platform thread'den oluşan bir taşıyıcı (carrier) havuzu üzerinde çalıştırılır. Bir virtual thread bloklandığında, taşıyıcı thread serbest bırakılır ve başka bir virtual thread'e hizmet eder. Sonuç: bloklayan kod yazmaya devam ederken yüksek eşzamanlılık elde edersiniz.
Platform Thread ile Farkı
| Özellik | Platform Thread | Virtual Thread |
|---|---|---|
| Bağlı olduğu kaynak | OS thread | JVM tarafından yönetilir |
| Maliyet | Yüksek | Düşük |
| Pratik üst sınır | Binler | Milyonlar (bellek elverdiğince) |
| Bloklandığında | OS thread bekler | Taşıyıcı serbest kalır |
Önkoşullar ve Sürüm Kapsamı
- JDK 21 veya üzeri gerekir. Virtual threads Java 21'de standart hale geldi (öncesinde preview özelliğiydi).
- API
java.lang.Threadvejava.util.concurrent.Executorsüzerinden erişilir; ek bağımlılık gerekmez. - Framework desteği sürüme bağlıdır. Kullandığınız sürümün virtual thread desteğini kendi dokümantasyonundan doğrulayın; bu yazı belirli bir framework sürümü için davranış garantisi vermez.
Virtual Thread Oluşturma
En basit kullanım Thread.ofVirtual() iledir:
Thread vthread = Thread.ofVirtual().start(() -> {
System.out.println("Çalışan thread: " + Thread.currentThread());
});
vthread.join();
Thread.currentThread() çıktısında virtual thread olduğu görülür. Bu kod JDK 21 ile derlenip çalıştırılabilir; beklenen davranış, görevin bir virtual thread üzerinde yürütülmesi ve join() sonrası ana thread'in devam etmesidir.
ExecutorService ile Toplu Kullanım
Tek tek thread oluşturmak yerine, görev havuzu yaklaşımı daha pratiktir:
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
// bloklayan bir I/O işlemi simülasyonu
Thread.sleep(1000);
return null;
});
}
} // try-with-resources kapanışta tüm görevlerin bitmesini bekler
Burada her görev kendi virtual thread'inde çalışır. newVirtualThreadPerTaskExecutor() her görev için yeni bir virtual thread oluşturur ve havuzda tutmaz; virtual thread'ler zaten ucuz olduğu için bu yaklaşım önerilir. try-with-resources bloğu kapanırken executor görevlerin tamamlanmasını bekler.
Beklenen davranış: 10.000 görev de yaklaşık aynı anda bloklanır, ancak bunu taşıyan platform thread sayısı çok daha azdır. Bu, klasik sabit boyutlu havuzlarla pratik olmayan bir eşzamanlılık seviyesidir.
Mikroservis Etkileri
Virtual threads'in mikroservis mimarisindeki en doğrudan etkisi, istek başına thread modelinin yeniden uygulanabilir hale gelmesidir.
Bloklayan I/O'nun Maliyeti Düşer
Mikroservisler tipik olarak bir isteği işlerken birden fazla dış çağrı yapar: başka servisler, veritabanı, önbellek, kuyruk. Reaktif programlama (WebFlux, CompletableFuture zincirleri) bu bloklamayı önlemek için tercih ediliyordu, ancak kod karmaşıklığını artırıyordu. Virtual threads ile aynı senaryoyu düz, sıralı kodla yazabilirsiniz:
public OrderResponse getOrder(Long id) {
User user = userClient.fetch(id); // bloklayan HTTP çağrısı
List<Item> items = itemClient.list(id); // bloklayan HTTP çağrısı
return new OrderResponse(user, items);
}
Bu metot bir virtual thread üzerinde çalıştığında, iki bloklayan çağrı sırasında taşıyıcı thread serbest kalır. Kod okunabilirliği korunur, eşzamanlılık kaybı yaşanmaz.
Throughput ve Kaynak Kullanımı
Sabit thread havuzlu bir serviste eşzamanlılık thread sayısıyla sınırlıdır. Yavaş bir bağımlılık, havuzu doldurup diğer istekleri kuyruğa sokabilir. Virtual threads bu darboğazı hafifletir: her istek kendi virtual thread'inde bloklanabilir, havuz tükenmez.
Ancak bu bir sihirli değnek değildir. Darboğaz thread'den veritabanı bağlantı havuzuna, dış servisin kapasitesine veya CPU'ya kayabilir. Virtual threads, sınırlı bir kaynağa (örneğin 20 bağlantılık bir connection pool) yapılan çağrıları çoğaltırsa, binlerce virtual thread aynı havuzda bekler ve fayda sağlamaz.
CPU-Bound İşler İçin Uygun Değil
Virtual threads bloklayan I/O için tasarlandı. Yoğun CPU kullanan işler (kriptografi, büyük veri işleme, sıkı döngüler) için fayda sağlamaz; çünkü bu durumda taşıyıcı thread gerçekten meşguldür ve serbest bırakılmaz. CPU-bound işler için paralel stream'ler veya sabit boyutlu platform thread havuzları daha uygundur.
Yaygın Hatalar
1. Havuzlama
Virtual thread'leri havuzlamak yaygın bir hatadır. Virtual thread'ler ucuzdur ve havuzlanmak için değil, gerektiğinde oluşturulmak için tasarlanmıştır. Executors.newFixedThreadPool benzeri bir yapıyı virtual thread'lerle taklit etmek fayda sağlamaz.
2. synchronized Bloklarında Pinning
Bir virtual thread, synchronized bloğu içinde bloklandığında taşıyıcı thread'ini bırakamaz. Buna pinning denir. Sonuç, taşıyıcı havuzunun tükenmesi ve performans kaybıdır. Çözüm, kritik bölümler için ReentrantLock kullanmaktır:
private final ReentrantLock lock = new ReentrantLock();
public void update() {
lock.lock();
try {
// kritik bölge
} finally {
lock.unlock();
}
}
JDK 21'de synchronized içinde pinning hâlâ mümkündür. JDK'nın ilerleyen sürümlerinde bu davranışın iyileştirildiği belirtilmiştir; kullandığınız sürümün davranışını kendi dokümantasyonundan doğrulayın.
3. Thread-Local Aşırı Kullanımı
Her virtual thread kendi ThreadLocal kopyasını taşır. Milyonlarca virtual thread ile birlikte ağır ThreadLocal kullanımı bellek tüketimini artırır. Kısa ömürlü isteklerde thread-local'a yazılan veriler için ScopedValue (JDK 21'de preview) alternatif olarak değerlendirilebilir; ancak standart hale gelmeden üretimde kullanmadan önce sürüm durumunu kontrol edin.
4. Sınırsız Eşzamanlılık
newVirtualThreadPerTaskExecutor her görevi sınırsız kabul eder. Dış bir servise veya veritabanına yapılan çağrılarda eşzamanlılığı sınırlamazsanız, bağımlılığı boğabilirsiniz. Bu durumda bir Semaphore ile eşzamanlılığı sınırlamak gerekir.
Güvenlik Etkileri
- Kaynak tüketimi (DoS): Sınırsız virtual thread, bir saldırganın çok sayıda eşzamanlı istek göndermesi durumunda bellek ve dış bağımlılık kaynaklarını tüketebilir. Rate limiting ve eşzamanlılık sınırı (semaphore, connection pool) savunma katmanı olarak gereklidir.
- Thread-local sızıntısı: İstek bağlamında saklanan kimlik doğrulama bilgileri thread-local'da tutuluyorsa, virtual thread yaşam döngüsü boyunca erişilebilir kalır. İstek bittiğinde temizlendiğinden emin olun.
- Zaman aşımları: Virtual threads, bloklayan çağrıların zaman aşımı davranışını değiştirmez. Dış çağrılara açık timeout koymazsanız, binlerce virtual thread aynı yavaş bağımlılıkta bekleyebilir.
Ne Zaman Kullanmalı, Ne Zaman Kaçınmalı
Kullanın:
- İstek başına iş yükü yüksek, I/O yoğun servisler
- Bloklayan kütüphanelerle (JDBC, senkron HTTP istemcileri) çalışırken
- Kod okunabilirliğini reaktif zincirlerden daha önemli gördüğünüzde
Kaçının:
- CPU-bound iş yükleri
- Yoğun
synchronizedkullanan eski kütüphanelerle çalışırken (pinning riski) - Sınırlı bir bağımlılığa (connection pool) yapılan çağrıları sınırsız çoğaltmanın anlamlı olmadığı durumlarda
Sonuç
Virtual threads, Java'da bloklayan kod yazarken yüksek eşzamanlılık elde etmeyi mümkün kılar. Mikroservislerde en büyük katkısı, reaktif karmaşıklığa girmeden istek başına thread modelini sürdürülebilir kılmasıdır. Ancak faydayı görmek için doğru kullanım gerekir: havuzlamayın, pinning'den kaçının, eşzamanlılığı sınırlayın ve darboğazın thread'den başka bir yere kaymadığından emin olun.