Virtual Threads (Java 21+)
Giriş — Thread Problemi
Bir restoran düşün. Her müşteriye bir garson atıyorsun. 10 müşteri geldiğinde 10 garson, 100 müşteriye 100 garson. Ama restoranın kapasitesi 200 garson. 201. müşteri geldiğinde? Kapıda bekler. Garsonların çoğu ne yapıyor? Mutfağın yemeği hazırlamasını bekliyor. Yani garsonların %90'ı aktif çalışmıyor — sadece bekliyor.
İşte Java'nın klasik thread modeli tam olarak bu. Her HTTP isteğine bir thread atanır. Thread'lerin çoğu CPU'da hesaplama yapmıyor — veritabanı cevabını, HTTP çağrısını, dosya I/O'sunu bekliyor. Ama her thread ~1MB stack memory tutuyor ve OS seviyesinde kaynak tüketiyor.
Virtual threads bu problemi kökünden çözer: milyonlarca hafif thread oluşturabilirsin ve bekleyen thread'ler neredeyse sıfır kaynak tüketir.
Platform Thread vs Virtual Thread
Platform Thread (Klasik)
Java'da new Thread() veya ExecutorService ile oluşturduğun her thread, aslında bir OS thread'i ile 1:1 eşleşir:
Java Thread ←→ OS Thread ←→ CPU CoreHer platform thread ~1MB stack memory tüketir
OS thread oluşturma ve yok etme pahalı bir işlem (~1ms)
OS genelde birkaç bin thread'i yönetebilir
Context switch (thread değiştirme) maliyeti yüksek
Tipik bir Spring Boot uygulamasında Tomcat 200 thread ile gelir. 201. istek geldiğinde? Kuyrukta bekler. 200 thread'in 190'ı veritabanı cevabını bekliyorsa? Kalan 10 thread ile 200 concurrent kullanıcı sınırındasın.
Virtual Thread (Java 21+)
Virtual thread'ler JVM tarafından yönetilen hafif thread'lerdir. OS thread'leri ile M:N eşleşme yapar:
Virtual Thread 1 ─┐
Virtual Thread 2 ─┤
Virtual Thread 3 ─┼──→ Carrier Thread (OS Thread) ──→ CPU Core
Virtual Thread 4 ─┤
Virtual Thread 5 ─┘Her virtual thread ~1KB memory (platform thread'in ~1000'de biri)
Oluşturma maliyeti neredeyse sıfır (~1μs)
Milyonlarca virtual thread oluşturabilirsin
Bloklandığında (I/O beklerken) carrier thread'i serbest bırakır — başka virtual thread çalışır
Garson analojisine dönelim: Virtual thread'ler, garson yerine sipariş notu sistemi gibi. 1000 sipariş notu olabilir, ama mutfak hazır olduğunda ilgili not alınıp servis yapılır. 200 garson limiti yok.
Thread-Per-Request Model Problemi
Klasik Model
// Tomcat her istek için bir thread atar
@GetMapping("/api/orders/{id}")
public Order getOrder(@PathVariable Long id) {
// Thread bloklandı — DB cevabını bekliyor (~5ms)
Order order = orderRepository.findById(id).orElseThrow();
// Thread bloklandı — başka servis cevabını bekliyor (~50ms)
UserInfo user = userServiceClient.getUser(order.getUserId());
// Thread bloklandı — ödeme bilgisi (~30ms)
PaymentInfo payment = paymentServiceClient.getPayment(order.getPaymentId());
order.setUserInfo(user);
order.setPaymentInfo(payment);
return order;
// Toplam: ~85ms bloklanma, ~0.1ms CPU çalışması
// Thread zamanının %99.9'u BEKLEMEde geçti
}200 thread ile saniyede kaç istek handle edebilirsin?
200 thread × (1000ms / 85ms) ≈ 2350 request/saniyeGerçekte overhead'ler ile ~1500-2000 request/saniye. Ama thread'lerin %99.9'u CPU çalışmıyor, bekliyor. CPU boşta duruyor ama daha fazla isteğe cevap veremiyorsun. Bu C10K problemi — 10,000 concurrent bağlantıyı handle etme zorluğu.
Çözüm Denemeleri (Virtual Thread Öncesi)
Thread sayısını artır: 200 → 2000? Memory patlar (2GB sadece thread stack'leri)
Reactive (WebFlux): Callback/mono/flux ile non-blocking kod. Çalışır ama kodun okunaksız hale gelir, debugging kabus olur
Virtual Threads: Thread-per-request modelini koru, ama binlerce yerine milyonlarca thread kullan ✅
Project Loom Motivasyonu
Project Loom, Java platform ekibinin yıllarca süren çalışmasının ürünü. Motivasyon basit: mevcut Java kodunu değiştirmeden, ölçeklenebilirliği dramatik artırmak.
Reactive programming (WebFlux, RxJava) teoride aynı problemi çözer. Ama pratikte:
// Reactive (WebFlux) — callback zinciri
public Mono<Order> getOrder(Long id) {
return orderRepository.findById(id)
.flatMap(order ->
Mono.zip(
userServiceClient.getUser(order.getUserId()),
paymentServiceClient.getPayment(order.getPaymentId())
).map(tuple -> {
order.setUserInfo(tuple.getT1());
order.setPaymentInfo(tuple.getT2());
return order;
})
)
.switchIfEmpty(Mono.error(new OrderNotFoundException(id)));
}
// Virtual Threads — normal, senkron kod
public Order getOrder(Long id) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
order.setUserInfo(userServiceClient.getUser(order.getUserId()));
order.setPaymentInfo(paymentServiceClient.getPayment(order.getPaymentId()));
return order;
}İkisi de aynı performansı veriyor. Ama virtual thread versiyonu:
Okunabilir — düz, senkron kod
Debug edilebilir — normal stack trace
Test edilebilir — standart JUnit
Öğrenme eğrisi düşük — yeni paradigma öğrenmeye gerek yok
💡 İpucu: Virtual threads, reactive programming'in yerini almaz. Eğer zaten WebFlux kullanıyorsan ve memnunsan, geçiş yapma. Ama yeni projelerde veya klasik Spring MVC projelerinde virtual threads çok daha pragmatik bir çözüm.
Virtual Thread Oluşturma
Temel API (Java 21)
// 1. Thread.ofVirtual() ile oluşturma
Thread vThread = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() -> {
System.out.println("Running on: " + Thread.currentThread());
// Virtual thread olduğunu kontrol et
System.out.println("Is virtual: " + Thread.currentThread().isVirtual());
});
vThread.join(); // Bitmesini bekle
// 2. Thread.startVirtualThread() — kısa yol
Thread vt = Thread.startVirtualThread(() -> {
System.out.println("Quick virtual thread");
});
// 3. ExecutorService ile (Önerilen)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Her submit() yeni bir virtual thread oluşturur
Future<String> future1 = executor.submit(() -> fetchFromDB());
Future<String> future2 = executor.submit(() -> callExternalAPI());
Future<String> future3 = executor.submit(() -> readFromFile());
// Sonuçları topla
String dbResult = future1.get();
String apiResult = future2.get();
String fileResult = future3.get();
}100.000 Virtual Thread Oluşturma (Demo)
public class VirtualThreadDemo {
public static void main(String[] args) throws Exception {
// Platform thread ile 100.000 thread oluşturmayı dene → OutOfMemoryError
// Virtual thread ile → birkaç saniye
Instant start = Instant.now();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
final int taskId = i;
futures.add(executor.submit(() -> {
// I/O simülasyonu — 1 saniye bekleme
Thread.sleep(Duration.ofSeconds(1));
return "Task " + taskId + " completed on " + Thread.currentThread();
}));
}
// Tüm sonuçları topla
int completed = 0;
for (Future<String> future : futures) {
future.get();
completed++;
}
System.out.println("Completed: " + completed);
}
Duration elapsed = Duration.between(start, Instant.now());
System.out.println("Total time: " + elapsed.toMillis() + "ms");
// 100.000 task × 1 saniye bekleme → ~1-2 saniye (virtual threads ile)
// Platform thread ile? Saatler sürer veya OutOfMemoryError
}
}⚠️ Dikkat:
Executors.newVirtualThreadPerTaskExecutor()her task için yeni virtual thread oluşturur. KlasiknewFixedThreadPool(200)gibi sınırlama yapmaz. Bu tasarımsal bir karar — virtual thread'ler ucuz, sınırlamaya gerek yok. Ama bu, downstream kaynaklar (DB connection pool, external API rate limit) için dikkatli olmanı gerektirir.
Spring Boot 3.2+ Entegrasyonu
Spring Boot 3.2'den itibaren virtual thread desteği tek bir property ile aktif edilebilir:
# application.properties
spring.threads.virtual.enabled=trueBu tek satır, uygulamanın üç temel alanını değiştirir:
1. Tomcat Handler Threads → Virtual Threads
Varsayılan davranışta Tomcat 200 platform thread kullanır. spring.threads.virtual.enabled=true ile:
Her HTTP isteği için yeni bir virtual thread oluşturulur
200 thread limiti kalkar
Binlerce concurrent isteği handle edebilirsin
Kodunda hiçbir değişiklik yapmana gerek yok — controller'lar, service'ler, repository'ler aynen çalışır.
2. @Async → Virtual Thread Executor
@Async
public CompletableFuture<EmailResult> sendEmail(String to, String body) {
// Bu metot artık virtual thread'de çalışır
emailClient.send(to, body);
return CompletableFuture.completedFuture(new EmailResult(true));
}spring.threads.virtual.enabled=true aktifken, Spring'in @Async metodları artık varsayılan olarak SimpleAsyncTaskExecutor kullanır ve her task için virtual thread oluşturur.
3. Scheduled Tasks → Virtual Threads
@Scheduled(fixedRate = 60000)
public void cleanupExpiredSessions() {
// Bu da virtual thread'de çalışır
sessionService.deleteExpired();
}Minimal Konfigürasyon Örneği
@SpringBootApplication
public class ECommerceApplication {
public static void main(String[] args) {
SpringApplication.run(ECommerceApplication.class, args);
}
}# application.properties
spring.application.name=ecommerce
spring.threads.virtual.enabled=true
# Tomcat artık virtual thread kullanıyor
# Eski max-threads ayarı artık farklı anlam taşır
# server.tomcat.threads.max=200 ← buna gerek yok# application.yml
spring:
application:
name: ecommerce
threads:
virtual:
enabled: trueBu kadar. Mevcut @RestController, @Service, @Repository sınıfların aynen çalışmaya devam eder ama artık her istek virtual thread'de çalışır.
💡 İpucu: Spring Boot 3.2+ ve Java 21+ gerektiriyor.
pom.xml'de Java versiyonunu kontrol et: ``xml <properties> <java.version>21</java.version> </properties>``
Pinning Problemi — Dikkat!
Virtual thread'lerin en kritik tuzağı pinning. Virtual thread normalde carrier thread'i serbest bırakır (I/O beklerken başka virtual thread çalışabilsin diye). Ama `synchronized` bloğu içindeyken bunu yapamaz — carrier thread'e pinlenir.
Problem
// ❌ TEHLİKELİ — Virtual thread pinning
public class LegacyService {
private final Object lock = new Object();
public String fetchData() {
synchronized (lock) {
// Bu HTTP çağrısı sırasında virtual thread carrier thread'e pinlenir
// Carrier thread bloklanır — virtual thread'in avantajı kaybolur
return httpClient.get("https://api.example.com/data");
}
}
}Pinning olduğunda ne olur?
Virtual thread carrier thread'i bırakamaz
Carrier thread havuzu tıkanır (genelde CPU çekirdeği sayısı kadar carrier thread var)
Diğer virtual thread'ler çalışamaz
Sonuç: performans çöker, virtual thread'in tüm avantajı kaybolur
Çözüm: ReentrantLock Kullan
// ✅ DOĞRU — ReentrantLock ile virtual thread carrier'ı serbest bırakır
public class ModernService {
private final ReentrantLock lock = new ReentrantLock();
public String fetchData() {
lock.lock();
try {
// Bu HTTP çağrısı sırasında virtual thread carrier'ı serbest bırakır
// Diğer virtual thread'ler çalışmaya devam eder
return httpClient.get("https://api.example.com/data");
} finally {
lock.unlock();
}
}
}Pinning Tespiti
JVM'e -Djdk.tracePinnedThreads=full parametresi ekleyerek pinning olduğunda stack trace alabilirsin:
java -Djdk.tracePinnedThreads=full -jar myapp.jarPinning oluştuğunda konsolda şöyle bir uyarı görürsün:
Thread[#42,ForkJoinPool-1-worker-3,5,CarrierThreads]
parking while pinned
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:180)
...
com.myapp.LegacyService.fetchData(LegacyService.java:15) <== synchronizedYaygın Pinning Kaynakları
| Kaynak | Çözüm |
|---|---|
synchronized bloğu | ReentrantLock kullan |
synchronized metot | ReentrantLock kullan |
| Eski JDBC driver'ları | Güncel sürüme yükselt |
| JNI (native code) | Genelde değiştirilemez — kabul et |
// ❌ synchronized metot
public synchronized void process() {
dbCall();
}
// ✅ ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
public void process() {
lock.lock();
try {
dbCall();
} finally {
lock.unlock();
}
}⚠️ Dikkat: Kendi kodundaki
synchronized'ı düzeltebilirsin ama kullandığın kütüphanelerdeki pinning'i düzeltemezsin. Özellikle eski JDBC driver'ları (MySQL 5.x driver gibi) internal olaraksynchronizedkullanır. Güncel driver'lar bu sorunu büyük ölçüde çözdü.
Performans Karşılaştırma
Aşağıdaki benchmark, platform thread ve virtual thread arasındaki farkı gösteriyor. Senaryo: her isteğin 100ms I/O bekleme yapan basit bir HTTP endpoint.
Benchmark Kodu
@RestController
@RequestMapping("/api/benchmark")
public class BenchmarkController {
@GetMapping("/io-task")
public String ioTask() throws InterruptedException {
// Veritabanı veya external API çağrısı simülasyonu
Thread.sleep(100);
return "completed by " + Thread.currentThread();
}
}Sonuçlar (Simülasyon)
| Metrik | Platform Thread (200) | Virtual Thread |
|---|---|---|
| Concurrent bağlantı | 200 | 10,000+ |
| Throughput | ~1,900 req/s | ~9,500 req/s |
| p99 latency | ~850ms | ~115ms |
| Memory kullanımı | ~400MB (thread stacks) | ~50MB |
| Thread sayısı | 200 (sabit) | İhtiyaç kadar |
5x throughput artışı, 7x daha düşük latency, 8x daha az memory. Ve kodda tek satır değişiklik yok — sadece spring.threads.virtual.enabled=true.
Basit Load Test Scripti
# wrk ile benchmark (10 thread, 1000 connection, 30 saniye)
# Platform threads
wrk -t10 -c1000 -d30s http://localhost:8080/api/benchmark/io-task
# Virtual threads (aynı uygulamayı spring.threads.virtual.enabled=true ile)
wrk -t10 -c1000 -d30s http://localhost:8080/api/benchmark/io-task💡 İpucu: Virtual thread'lerin gerçek faydası I/O-bound iş yüklerinde ortaya çıkar. Eğer endpoint'in sadece CPU hesaplaması yapıyorsa (image processing, şifreleme, matematiksel hesaplama), virtual thread ile platform thread arasında fark görmezsin. Çünkü CPU-bound işlerde darboğaz CPU'dur, thread değil.
Structured Concurrency (Preview)
Java 21'de preview olarak gelen Structured Concurrency, paralel çalışan task'ları bir arada yönetmeyi kolaylaştırır. Ana fikir: "birden fazla async task başlat, hepsi bitene kadar bekle, biri hata verirse hepsini iptal et."
// --enable-preview flag'i gerektirir
import java.util.concurrent.StructuredTaskScope;
public class OrderAggregator {
OrderDetails getOrderDetails(Long orderId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 3 paralel task başlat
StructuredTaskScope.Subtask<Order> orderTask =
scope.fork(() -> orderService.getOrder(orderId));
StructuredTaskScope.Subtask<UserInfo> userTask =
scope.fork(() -> userService.getUser(orderId));
StructuredTaskScope.Subtask<PaymentInfo> paymentTask =
scope.fork(() -> paymentService.getPayment(orderId));
// Hepsi bitene kadar bekle (veya biri hata verene kadar)
scope.join();
// Hata varsa fırlat
scope.throwIfFailed();
// Sonuçları topla
return new OrderDetails(
orderTask.get(),
userTask.get(),
paymentTask.get()
);
}
// scope kapanınca tüm subtask'lar da kapanır
// Bir task hata verirse diğerleri otomatik iptal edilir
}
}ShutdownOnFailure stratejisi: herhangi bir task hata verirse, diğer tüm task'ları iptal et ve hatayı fırlat. Alternatif olarak ShutdownOnSuccess var — ilk başarılı sonucu al, diğerlerini iptal et (race pattern).
// İlk başarılı sonucu al (ör: birden fazla kaynaktan arama)
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
scope.fork(() -> searchInCache(query));
scope.fork(() -> searchInDatabase(query));
scope.fork(() -> searchInElasticsearch(query));
scope.join();
// İlk dönen başarılı sonuç
String result = scope.result();
}⚠️ Dikkat: Structured Concurrency Java 21'de preview özelliğidir. Production'da kullanmak için
--enable-previewflag'i gerekir. Java 24+ sürümlerinde final olması bekleniyor. Spring Boot henüz doğrudan entegre etmemiş — kendi kodunda kullanabilirsin.
Ne Zaman Virtual Thread Kullanılır?
✅ Virtual Thread'ler İçin İdeal Senaryolar
✅ HTTP istekleri handle etme (Spring MVC controller'lar)
✅ Veritabanı sorguları (JDBC, JPA)
✅ External API çağrıları (RestTemplate, WebClient blocking)
✅ Dosya I/O işlemleri
✅ Message queue okuma/yazma (Kafka, RabbitMQ consumer)
✅ Email gönderme
✅ Cache operasyonları (Redis)Ortak özellik: I/O-bound — thread'in çoğu zaman bir şeyin cevabını beklediği işler.
❌ Virtual Thread'lerin Fayda Sağlamadığı Senaryolar
❌ CPU-intensive hesaplamalar (image processing, video encoding)
❌ Matematiksel modelleme, şifreleme
❌ In-memory veri işleme (büyük collection'lar üzerinde sort, filter)
❌ Machine learning inferenceCPU-bound işlerde darboğaz thread sayısı değil, CPU çekirdek sayısıdır. 8 çekirdekli bir makinede 8 CPU-bound task paralel çalışır — 1000 virtual thread oluştursan bile.
// ❌ Bu senaryoda virtual thread fayda sağlamaz
@GetMapping("/api/hash")
public String computeHash(@RequestParam String data) {
// Pure CPU işlemi — I/O bekleme yok
return BCrypt.hashpw(data, BCrypt.gensalt(14));
}
// ✅ Bu senaryoda virtual thread çok faydalı
@GetMapping("/api/order/{id}")
public OrderDetails getOrderDetails(@PathVariable Long id) {
// 3 farklı I/O çağrısı — thread zamanının %99'u bekleme
Order order = orderRepo.findById(id).orElseThrow();
User user = userClient.getUser(order.getUserId());
Payment payment = paymentClient.getPayment(id);
return new OrderDetails(order, user, payment);
}Virtual Threads ve Connection Pool — HikariCP Dikkat!
Virtual thread'lerin en büyük tuzaklarından biri: downstream kaynakların sınırlarını aşma.
Eskiden Tomcat 200 thread ile çalışıyordu. 200 thread → HikariCP 10 connection = sorun yok, çünkü en fazla 200 istek aynı anda DB'ye gider.
Ama virtual thread ile? 10.000 concurrent istek → 10.000 virtual thread → hepsi aynı anda HikariCP'den connection istiyor → HikariCP 10 connection ile tıkanır.
Problem Senaryosu
spring.threads.virtual.enabled=true
spring.datasource.hikari.maximum-pool-size=1010.000 concurrent istek geldiğinde:
10 virtual thread DB connection alır
9.990 virtual thread
HikariPool.getConnection()çağrısında beklerConnection timeout (30 saniye) dolunca hata fırlatar
Kullanıcılar
Connection is not availablehatası görür
Çözüm Stratejileri
# 1. Connection pool boyutunu artır (ama DB'nin de desteklemesi lazım!)
spring.datasource.hikari.maximum-pool-size=50
# 2. Connection timeout'u ayarla
spring.datasource.hikari.connection-timeout=10000
# 3. DB tarafındaki max_connections'ı kontrol et
# PostgreSQL: max_connections = 100 (varsayılan)
# MySQL: max_connections = 151 (varsayılan)// Semaphore ile concurrent DB erişimini sınırla
@Service
public class OrderService {
// Aynı anda en fazla 50 virtual thread DB'ye erişebilir
private static final Semaphore DB_SEMAPHORE = new Semaphore(50);
public Order getOrder(Long id) {
try {
DB_SEMAPHORE.acquire();
return orderRepository.findById(id).orElseThrow();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
DB_SEMAPHORE.release();
}
}
}⚠️ Dikkat: Virtual thread'ler "sınırsız" thread oluşturmanı sağlar, ama downstream kaynaklar (veritabanı, external API, Redis) hâlâ sınırlı. Backpressure mekanizması kur — Semaphore, rate limiter veya connection pool boyutunu uygun ayarla.
Connection Pool Boyutu Formülü
pool_size = (core_count * 2) + disk_spindle_countAma virtual thread ile farklı düşünmen gerekiyor:
pool_size = min(DB max_connections / app_instance_count, expected_concurrent_db_calls)Örnek: PostgreSQL max_connections=200, 4 app instance → her instance max 50 connection.
# Production önerisi
spring.datasource.hikari.maximum-pool-size=50
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.connection-timeout=5000
spring.datasource.hikari.idle-timeout=300000Migration Guide — Mevcut Uygulamaya Virtual Threads Ekleme
Mevcut bir Spring Boot uygulamasına virtual thread eklemek genelde kolaydır, ama dikkat edilmesi gereken noktalar var.
Adım 1: Java 21+ ve Spring Boot 3.2+
<!-- pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version> <!-- veya üzeri -->
</parent>
<properties>
<java.version>21</java.version>
</properties>Adım 2: Virtual Thread'leri Aktif Et
# application.properties
spring.threads.virtual.enabled=trueAdım 3: synchronized Kullanımlarını Tara
# Projende synchronized kullanımlarını bul
grep -rn "synchronized" src/main/java/Her synchronized bloğunu ReentrantLock ile değiştir:
// Önceki
private final Object lock = new Object();
public void criticalSection() {
synchronized (lock) {
doWork();
}
}
// Sonraki
private final ReentrantLock lock = new ReentrantLock();
public void criticalSection() {
lock.lock();
try {
doWork();
} finally {
lock.unlock();
}
}Adım 4: ThreadLocal Kullanımlarını Kontrol Et
Virtual thread'ler milyonlarca olabilir. ThreadLocal her thread için ayrı kopya tutar — milyonlarca kopya = memory patlaması.
// ❌ Dikkat — milyonlarca virtual thread ile memory sorunu
private static final ThreadLocal<ExpensiveObject> cache =
ThreadLocal.withInitial(ExpensiveObject::new);
// ✅ Alternatif — ScopedValue (Java 21 Preview)
private static final ScopedValue<RequestContext> CONTEXT = ScopedValue.newInstance();Adım 5: Connection Pool Ayarlarını Gözden Geçir
# Önceki (platform threads ile)
spring.datasource.hikari.maximum-pool-size=10
# Virtual threads ile — artırmayı düşün
spring.datasource.hikari.maximum-pool-size=50Adım 6: Pinning Tespiti ile Test Et
# Test ortamında pinning tespiti aç
java -Djdk.tracePinnedThreads=short -jar myapp.jarAdım 7: Load Test
# Önceki performans baseline (platform threads)
wrk -t10 -c500 -d60s http://localhost:8080/api/orders
# Virtual threads aktif
wrk -t10 -c500 -d60s http://localhost:8080/api/orders
# High concurrency test
wrk -t10 -c5000 -d60s http://localhost:8080/api/ordersMigration Checklist
☐ Java 21+ ve Spring Boot 3.2+ yükseltme
☐ spring.threads.virtual.enabled=true ekleme
☐ synchronized → ReentrantLock dönüşümü
☐ ThreadLocal kullanımlarını gözden geçirme
☐ Connection pool boyutunu ayarlama
☐ JDBC driver'ı güncelleme (pinning tespiti)
☐ Pinning testi (-Djdk.tracePinnedThreads)
☐ Load test (before/after karşılaştırma)
☐ Memory profiling (virtual thread stack'leri)Gerçek Dünya Örneği: 1000 Concurrent HTTP Request
Bir e-ticaret uygulamasında ürün detay sayfası 3 farklı servisten veri çekiyor:
Product Service — ürün bilgisi
Review Service — kullanıcı yorumları
Recommendation Service — önerilen ürünler
Her servis ~100ms cevap veriyor.
Before — Platform Threads (200 thread)
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final RestTemplate restTemplate;
@GetMapping("/{id}")
public ProductPage getProductPage(@PathVariable Long id) {
// Sıralı çağrılar — 300ms toplam
Product product = restTemplate.getForObject(
"http://product-service/products/" + id, Product.class);
List<Review> reviews = restTemplate.getForObject(
"http://review-service/products/" + id + "/reviews",
new ParameterizedTypeReference<List<Review>>() {});
List<Product> recommendations = restTemplate.getForObject(
"http://recommendation-service/products/" + id + "/similar",
new ParameterizedTypeReference<List<Product>>() {});
return new ProductPage(product, reviews, recommendations);
}
}# Platform threads — 200 thread limiti
server.tomcat.threads.max=200Sonuç:
200 concurrent kullanıcı handle edebilir
Her istek ~300ms (sıralı 3 çağrı)
201. kullanıcı kuyrukta bekler
Throughput: ~650 req/s
After — Virtual Threads + Parallel Calls
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final RestTemplate restTemplate;
@GetMapping("/{id}")
public ProductPage getProductPage(@PathVariable Long id) throws Exception {
// Paralel çağrılar — structured concurrency ile
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<Product> productFuture = executor.submit(() ->
restTemplate.getForObject(
"http://product-service/products/" + id, Product.class));
Future<List<Review>> reviewsFuture = executor.submit(() ->
restTemplate.getForObject(
"http://review-service/products/" + id + "/reviews",
new ParameterizedTypeReference<List<Review>>() {}));
Future<List<Product>> recommendationsFuture = executor.submit(() ->
restTemplate.getForObject(
"http://recommendation-service/products/" + id + "/similar",
new ParameterizedTypeReference<List<Product>>() {}));
return new ProductPage(
productFuture.get(5, TimeUnit.SECONDS),
reviewsFuture.get(5, TimeUnit.SECONDS),
recommendationsFuture.get(5, TimeUnit.SECONDS)
);
}
}
}# Virtual threads — sınır yok
spring.threads.virtual.enabled=trueSonuç:
10.000+ concurrent kullanıcı handle edebilir
Her istek ~100ms (paralel 3 çağrı — en yavaş kadar)
Kuyrukta bekleme yok (pratikte)
Throughput: ~9.000 req/s
CompletableFuture Alternatifi
Virtual thread'ler olmadan da paralel çağrı yapabilirsin, ama kod daha karmaşık:
// CompletableFuture ile (virtual thread olmadan da çalışır ama daha verbose)
@GetMapping("/{id}")
public ProductPage getProductPage(@PathVariable Long id) {
CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(() ->
restTemplate.getForObject("http://product-service/products/" + id, Product.class));
CompletableFuture<List<Review>> reviewsFuture = CompletableFuture.supplyAsync(() ->
restTemplate.getForObject("http://review-service/products/" + id + "/reviews",
new ParameterizedTypeReference<List<Review>>() {}));
CompletableFuture<List<Product>> recommendationsFuture = CompletableFuture.supplyAsync(() ->
restTemplate.getForObject("http://recommendation-service/products/" + id + "/similar",
new ParameterizedTypeReference<List<Product>>() {}));
CompletableFuture.allOf(productFuture, reviewsFuture, recommendationsFuture).join();
return new ProductPage(
productFuture.join(),
reviewsFuture.join(),
recommendationsFuture.join()
);
}💡 İpucu: Virtual thread'ler
CompletableFuture'ı gereksiz kılmaz. AmaExecutors.newVirtualThreadPerTaskExecutor()ileFuturekullanımı daha okunabilir. Structured Concurrency (preview) ise hata yönetimi ve iptal mekanizmasını çok daha temiz yapıyor.
Custom Virtual Thread Executor (İleri Seviye)
Bazen Spring Boot'un default konfigürasyonu yetmez. Kendi executor'ünü tanımlamak isteyebilirsin:
@Configuration
public class VirtualThreadConfig {
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
// @Async için custom executor
@Bean(name = "virtualThreadExecutor")
public Executor virtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
// İsimlendirilmiş virtual thread factory
@Bean
public ExecutorService namedVirtualThreadExecutor() {
return Executors.newThreadPerTaskExecutor(
Thread.ofVirtual()
.name("app-vt-", 0) // app-vt-0, app-vt-1, app-vt-2...
.factory()
);
}
}// Belirli bir executor kullan
@Async("virtualThreadExecutor")
public void processOrder(Long orderId) {
// Virtual thread'de çalışır
}Virtual Threads + Monitoring
Virtual thread'ler JMX ve thread dump'larda görünür, ama milyonlarca olabilecekleri için dikkatli olmalısın:
# Thread dump (virtual thread'ler de dahil)
jcmd <pid> Thread.dump_to_file -format=json threads.json
# Sadece pinned virtual thread'leri izle
java -Djdk.tracePinnedThreads=short -jar myapp.jarSpring Boot Actuator ile:
management.endpoints.web.exposure.include=health,metrics,threaddump# Metrics — aktif virtual thread sayısı
curl http://localhost:8080/actuator/metrics/jvm.threads.live
curl http://localhost:8080/actuator/metrics/jvm.threads.started💡 İpucu:
jcmd <pid> Thread.dump_to_fileile virtual thread dump alırken dosya boyutuna dikkat et. 100.000 virtual thread varsa dump dosyası çok büyük olabilir.-format=jsonile alıp jq gibi araçlarla filtrele.
Özet
Virtual thread'ler Java 21 ile gelen hafif thread'lerdir. Platform thread ~1MB iken virtual thread ~1KB memory tüketir. Milyonlarca oluşturabilirsin
Spring Boot 3.2+ tek satır ile entegrasyon:
spring.threads.virtual.enabled=true— Tomcat, @Async, Scheduling hepsi otomatik virtual thread kullanırI/O-bound iş yükleri için mükemmel (DB, HTTP, dosya I/O). CPU-bound işlerde fayda sağlamaz
Pinning tehlikesi:
synchronizedbloğu virtual thread'i carrier thread'e pinler.ReentrantLockkullan.-Djdk.tracePinnedThreadsile tespit etConnection pool dikkat: Virtual thread'ler sınırsız ama HikariCP sınırlı. Pool boyutunu artır, Semaphore ile sınırla veya downstream kaynak limitlerini hesapla
Migration kolay: Java 21 + Spring Boot 3.2 yükselt, property ekle, synchronized→ReentrantLock dönüşümü yap, load test et. Mevcut kodda başka değişiklik gerekmez
AI Asistan
Sorularını yanıtlamaya hazır