Java Virtual Threads: Milyonlarca Thread Nasıl Mümkün Oldu?
Java 21 ile birlikte Virtual Thread (Sanal İş Parçacığı) artık hayatımızın bir parçası. Aslında önceki sürümlerde preview olarak…
Java Virtual Threads: Milyonlarca Thread Nasıl Mümkün Oldu?
Java 21 ile birlikte Virtual Thread (Sanal İş Parçacığı) artık hayatımızın bir parçası. Aslında önceki sürümlerde preview olarak kullanabiliyorduk ancak Java 21 ile birlikte resmi olarak kullanıma sunuldu.
Peki şimdi ne yapacağız? Projelerimizi hemen Virtual Thread’e mi geçireceğiz? Sonuçta Virtual Thread’ler performans ve ölçeklenebilirlik konusunda oldukça iddialı geliyor. Ancak bu noktada biraz temkinli olmakta fayda var. Virtual Thread’e geçiş sonrasında beklenmedik performans problemleri veya üretim ortamında yaşanan sorunları anlatan birçok blog yazısı görmek mümkün. Bu nedenle doğrudan geçiş yapmak yerine önce Virtual Thread’lerin nasıl çalıştığını, hangi problemleri çözdüğünü ve hangi durumlarda dikkatli olunması gerektiğini anlamaya çalışalım. Sonrasında kendi uygulamalarımız için gerçekten doğru tercih olup olmadığına daha sağlıklı karar verebiliriz.

Görsel yapay zeka ile oluşturulmuştur.
Java’da yıllardır kullandığımız platform thread modeli başarılı olsa da, yüksek eşzamanlılık gerektiren uygulamalarda önemli bir sınıra sahipti. Her Java thread’i işletim sistemi seviyesinde bir thread ile eşlendiği için binlerce bekleyen isteğin bulunduğu sistemlerde bellek tüketimi ve scheduler maliyetleri hızla artıyordu. Özellikle HTTP servisleri, veritabanı çağrıları ve mesajlaşma sistemleri gibi I/O ağırlıklı uygulamalarda thread’lerin büyük bölümü aslında çalışmak yerine bekliyordu. Virtual Thread’ler tam olarak bu problemi çözmek amacıyla geliştirildi.
Virtual Thread’in tanımı şu şekilde; Java’da yüksek eşzamanlı(concurrent) uygulamaların yazılmasını, bakımını ve hata ayıklamasını kolaylaştıran, doğrudan işletim sistemi çekirdeğine bağlı olmak yerine JVM tarafından yönetilen hafif bir iş parçacığıdır.

Geleneksel thread modelinde bire bir ilişki vardır; yani Java tarafında bir thread oluşturulduğunda, aynı anda işletim sistemi tarafında da karşılığı oluşturulur. Thread yönetimi işletim sistemi tarafından yapıldığı için aynı anda çalıştırılabilecek thread sayısı sınırlı. Virtual Thread yaklaşımında bire çok ilişki bulunur. Binlerce hatta milyonlarca Virtual Thread oluşturulabilir ve JVM tarafından az sayıdaki platform thread üzerinde zamanlanarak çalıştırılır.
Java 8–17 döneminde kullandığımız klasik new Thread() veya ExecutorService thread’leri platform thread’tir. Her biri oluşturulduğunda JVM, OS’ten yeni bir native thread ister.
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread());
});
t.start();
Bu Thread objesi JVM içinde bir sarmalayıcıdır, ama altında bir native OS thread vardır.
Bir Virtual Thread (VT) çalışırken aslında kendi başına bir OS thread değildir. Virtual Thread CPU üzerinde doğrudan çalışamaz. Çalışabilmesi için mutlaka bir carrier thread üzerine mount edilmesi gerekir.
Virtual Thread’lerin avantajını anlamak için önce aynı senaryonun geleneksel platform thread modeliyle nasıl çalıştığına bakalım.
@GetMapping("/orders/{id}")
public OrderResponse getOrder(Long id) {
Customer customer = customerClient.get(id);
Product product = productClient.get(id);
return ...
}
Bu kod:
Tomcat Thread → Customer Call → Bekliyor → Product Call → Bekliyor
şeklinde çalışıyor. Platform Thread kullanıldığında bu bekleme süresi boyunca Tomcat thread’i işletim sistemi seviyesinde kaynak tutmaya devam eder. Virtual Thread yaklaşımında ise durum farklıdır.
Virtual Thread de ise; aynı kod değişmeden kalabilir. Ancak Virtual Thread beklemeye geçtiğinde JVM thread’i askıya alabilir ve carrier thread’i başka işlerde kullanabilir.
Aynı kod, farklı çalışma modeli
Akış şu şekilde:
- Virtual Thread (VT) çalışmaya başlar.
- JVM, VT’yi bir carrier thread (OS thread) üzerine mount eder.
- VT, carrier thread üzerinde normal Java kodunu çalıştırır (örneğin bir HTTP isteğini işler).
- Eğer VT, Thread.sleep(), Future.get(), veritabanı çağrısı veya ağ I/O'su gibi beklemeye neden olan bir noktaya gelirse, JVM VT'yi carrier thread'den ayırabilir (unmount).
- Carrier thread serbest kalır ve başka Virtual Thread’leri çalıştırmaya devam eder.
- Beklenen işlem tamamlandığında JVM, Virtual Thread’i tekrar uygun bir carrier thread üzerine mount ederek çalışmasını sürdürür.
Böylece tek bir OS thread yüzlerce VT’yi sırayla çalıştırabilir (işte bu yüzden VT’ler çok hafif!).
Virtual Thread neyi çözmüş oluyor?
Virtual Thread’ler özellikle bekleme (waiting/blocking) sürelerinde avantaj sağlar.
Aşağıdaki örnekte; thread’in yaptığı şey aslında “Beklemek”. Normal bir Thread (platform thread) bir işlem beklerken bile işletim sistemi seviyesinde kaynak tutar.
@GetMapping("/users")
public User getUser() {
return restClient.get(…); // 300 ms bekliyor veya repository.findById(id); // DB'den cevap bekliyor
}
Burada thread şunu yapıyor:
- HTTP isteğini gönderiyor
- Karşı taraftan cevap bekliyor
- Bu bekleme sırasında CPU çalışmıyor
- Ama thread hâlâ memory + OS resource tutuyor
Yani thread hiçbir şey yapmadan oturuyor. Eğer 10.000 request aynı anda bekliyorsa:
- 10.000 normal thread açmak çok pahalıdır
- Memory tüketir
- Context switching artar
- OS scheduler zorlanır
Virtual Thread’in olayı burada başlıyor. Virtual Thread beklemeye girince JVM şunu yapıyor:
“Tamam sen bekliyorsun. Seni park ediyorum. Carrier thread’i başka işe veriyorum.”
- Virtual Thread askıya alınır
- Arkadaki gerçek OS thread boşa çıkar
- Başka request çalıştırılır
Bu yüzden binlerce bekleyen işi çok daha az maliyetle yönetebiliriz.
Platform thread: 10000 HTTP request, 10000 OS thread, yüksek memory, yüksek scheduler maliyeti
Virtual Thread: 10000 HTTP request, 10000 Virtual Thread ama belki 20–30 gerçek OS thread
Çünkü çoğu bekleme durumunda.
Burada “daha az maliyetle” dediğimizde:
- daha az memory
- daha az OS maliyeti
- daha az thread management
- daha az context switch
anlamına geliyor.
Carrier Thread Scheduler Nasıl Çalışır?
Virtual Thread’leri çalıştıran carrier thread’ler JVM içerisindeki ForkJoinPool tabanlı bir scheduler tarafından yönetilir. Varsayılan olarak scheduler’ın paralellik seviyesi Runtime.getRuntime().availableProcessors() değeri ile ilişkilidir. Örneğin işletim sistemi 16 mantıksal işlemci raporluyorsa scheduler’ın paralellik seviyesi varsayılan olarak availableProcessors() değerine yakındır. Ancak bu değer Virtual Thread sayısını değil, aynı anda aktif çalışabilecek carrier thread sayısını ifade eder.
CPU-bound (işlemci yoğun) işlerde durum farklı
CPU yoğun işlerde; Video encoding, Image processing, Cryptography, Machine learning, büyük JSON parse, şifreleme, image processing, complex calculation, large loop ve compression… thread zaten sürekli CPU kullanır, beklemez. Bu durumda Virtual Thread’in “bekleyen işi ucuzlatmak” avantajı ortaya çıkmaz. Bu işlerde thread sürekli CPU kullanır. CPU %100 çalışıyor ve bekleme ve I/O yok.
Bu örnek CPU-bound örneğidir ve Thread hesaplama yapmak için sürekli meşguldur. Bu durumda thread hiç boş kalmaz. VT ise burda yer alamaz.
for (int i = 0; i < 1_000_000_000; i++) {
result += Math.sqrt(i);
}
Bu durumda Virtual Thread’in; Unmount, Mount, Park ve Unpark gibi avantajları devreye girmez. Çünkü zaten bekleyen bir thread yoktur.
Neden performans artışı sağlamıyor?
Virtual Thread’ler CPU performansını artıran bir teknoloji değildir. Amaçları işlemciyi hızlandırmak değil, bekleme süreleri boyunca thread kaynaklarını daha verimli kullanmaktır. Eğer makinede 8 CPU core varsa aynı anda gerçek anlamda çalışabilecek thread sayısı yaklaşık 8 dir. Siz 100.000 Virtual Thread oluştursanız bile CPU hala 8 çekirdek ile sınırlıdır. CPU çekirdeği sayısı değişmediği için; Virtual Thread burada sihir yaratmaz ve geri kalan threadler bekler. Dolayısıyla CPU-bound problemin çözümü Virtual Thread değildir.
Buraya kadar Virtual Thread’lerin neden daha ölçeklenebilir olduğunu gördük. Ancak akla doğal olarak şu soru geliyor: Eğer bir Virtual Thread sürekli olarak bir OS thread üzerinde yaşamıyorsa, stack bilgileri ve ThreadLocal gibi thread’e özel veriler nerede tutuluyor? Bu sorunun cevabı aynı zamanda Virtual Thread’lerin neden bu kadar hafif olduğunu da açıklıyor.
Platform thread’lerde stack belleği işletim sistemi tarafından önceden ayrılır ve her thread için önemli miktarda bellek tüketebilir. Çünkü her biri için OS:
- Stack belleği (örneğin 1 MB),
- Yönetim verileri (registers, scheduling, kernel context vs.) ayırır.
Bu nedenle çok sayıda eşzamanlı görev için binlerce thread oluşturmak maliyetlidir.
Virtual Thread’lerde ise stack frame’ler gerektiğinde heap üzerinde “stack chunk” olarak tutulur. Bir Virtual Thread beklemeye geçtiğinde JVM bu stack bilgisini heap’te saklayarak carrier thread’i serbest bırakır. ThreadLocal verileri stack’te değil, Thread nesnesinin içindeki ThreadLocalMap yapısında tutulduğu için Virtual Thread askıya alınsa bile korunur ve thread tekrar çalıştığında aynı şekilde erişilebilir.
Virtual Thread aşağıdaki durumlarda bloklanabilir
Bazı durumlarda JVM, sanal iş parçacığını OS thread’ten ayıramaz. Yani sanal thread, carrier thread’e “sabitlenir” (pinned) hale gelir. Bu tipik olarak şurada olur:
- Sanal thread, synchronized bir blok veya native (JVM dışı, örn. JNI) kod içindeyken
- Bloklayıcı bir işlem yaparsa (örneğin: I/O bekliyor, Future.get() çağrıyor, sleep() yapıyor)
JVM bu durumda şunu yapamaz:
“Bu VT bekliyor, OS thread’i serbest bırakayım.”
Çünkü synchronized blok içindeyken o lock’u bırakamaz; aksi takdirde veri tutarlılığı bozulur.
Sonuç:
- OS thread, VT’nin bitmesini beklemek zorunda kalır.
- Yani VT OS thread’e “pinned” hale gelir ve o thread başka iş yapamaz.
“Pinned olmak”, bir sanal iş parçacığının, geçici olarak üzerine bindiği (mounted olduğu) OS iş parçacığından ayrılamaması durumudur. JVM bu durumda, OS thread’i başka işlerde kullanamaz.
Etkisi nedir?
Virtual Threads’in amacı, çok sayıda görevi az sayıda OS thread üzerinde çoklayarak (multiplex) çalıştırmaktır. Ama eğer onlar pinned olursa:
- OS thread’ler boşta kalmaz, sürekli bekler,
- Havuz dolduğu için yeni VT’ler çalıştırılamaz,
- Yoğun pinning durumlarında carrier thread’ler uzun süre meşgul kalabilir ve sistemin ölçeklenebilirliği beklenenden daha düşük olabilir.
- Uzun süreli pinning durumlarında thread kaynakları verimsiz kullanılabilir, istek gecikmeleri artabilir ve bağlantıların zamanında kapanamaması gibi ikincil problemler gözlemlenebilir.
Not: Bu durum, synchronized kullanımının tamamen yanlış olduğu anlamına gelmez. Virtual Thread kullanırken de synchronized kullanılabilir. Ancak lock altında veritabanı çağrısı, HTTP isteği, dosya okuma/yazma, sleep() veya Future.get() gibi beklemeye neden olan işlemlerden kaçınılmalıdır. synchronized blokları mümkün olduğunca kısa tutulmalı ve yalnızca paylaşılan veriyi koruyan kritik bölgeyi kapsamalıdır.
Virtual Thread Spring Boot’ta nasıl açılır?
Spring Boot 3.2+ kullanıyorsanız; config olarak spring.threads.virtual.enabled=true ayarı vermeniz yeterli. Virtual Thread kullanmak, her servis çağrısı için Executors.newVirtualThreadPerTaskExecutor() oluşturmak anlamına gelmez. Spring Boot 3.2+ ile spring.threads.virtual.enabled=true ayarı kullanıldığında web request’leri doğrudan Virtual Thread’ler üzerinde çalıştırılabilir. VirtualThreadPerTaskExecutor ise genellikle aynı request içerisinde bağımsız işleri paralel çalıştırmak istediğimiz özel senaryolarda kullanılır.
Şimdi akla şu soru geldi “Ne Zaman Virtual Thread Kullanmalıyım?” Virtual Thread’ler en çok blocking I/O işlerinde faydalıdır. Virtual Thread’ler REST servisleri, Database erişimleri, Kafka veya MQ beklemeleri, Dosya işlemleri, External API çağrıları bu gibi senaryolarda oldukça başarılıdır.
Virtual Thread’leri bir performans sihri olarak görmek doğru değildir. Virtual Thread’ler CPU gücünü artırmaz, yeni çekirdekler oluşturmaz veya her uygulamayı otomatik olarak hızlandırmaz. Asıl amaçları, bekleyen (I/O-bound) işleri çok daha düşük maliyetle yönetebilmek ve geliştiricilerin tekrar “task başına thread” modeline yaklaşabilmesini sağlamaktır.
Ancak ThreadLocal kullanımı, synchronized bloklar, native çağrılar, connection pool limitleri ve concurrency kontrol mekanizmaları gözden geçirilmeden yapılan bir geçiş, beklenen faydayı sağlamayabilir. Özellikle pinning gibi durumlar, Virtual Thread’lerin ölçeklenebilirlik avantajını azaltabilir.
Bu yazı serisinde Virtual Thread’lerin ne olduğunu, nasıl çalıştığını, Spring Boot uygulamalarında nasıl kullanılabileceğini ve Carrier Thread Scheduler’ın arka planda nasıl çalıştığını incelemiş olduk.
Uzun yıllardır Java geliştiricileri thread’leri kıt bir kaynak olarak görmek zorundaydı. Thread pool boyutları, queue kapasiteleri ve concurrency limitleri bunun etrafında şekillendi. Virtual Thread’ler ile birlikte Java yeniden “task başına thread” modeline yaklaşmış durumda. Ancak bu değişim yalnızca yeni bir API değil; concurrency’yi düşünme biçimimizi değiştiren bir paradigma dönüşümü. Bir sonraki yazıda Virtual Thread vs concurrency’yi daha derinlemesine inceleyeceğiz.
Başka yazılarda görüşmek üzere.
Faydalandığım linklerden birini paylaşıyorum. Ref: netflixtechblog.com
메타데이터
- post_id
- dc7c49fd16fa
- slug
- java-virtual-threads-milyonlarca-thread-nasıl-mümkün-oldu-dc7c49fd16fa
- url
- https://medium.com/softtechas/java-virtual-threads-milyonlarca-thread-nas%C4%B1l-m%C3%BCmk%C3%BCn-oldu-dc7c49fd16fa
- canonical_url
- https://medium.com/softtechas/java-virtual-threads-milyonlarca-thread-nas%C4%B1l-m%C3%BCmk%C3%BCn-oldu-dc7c49fd16fa
- author_url
- https://medium.com/@didemagdogan
- status
- ok
- fetched_at
- 2026-06-17 08:20:12