← Kursa Dön
📄 Text · 20 min

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 Core
  • Her 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/saniye

Gerç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)

  1. Thread sayısını artır: 200 → 2000? Memory patlar (2GB sadece thread stack'leri)

  2. Reactive (WebFlux): Callback/mono/flux ile non-blocking kod. Çalışır ama kodun okunaksız hale gelir, debugging kabus olur

  3. 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. Klasik newFixedThreadPool(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=true

Bu 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: true

Bu 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?

  1. Virtual thread carrier thread'i bırakamaz

  2. Carrier thread havuzu tıkanır (genelde CPU çekirdeği sayısı kadar carrier thread var)

  3. Diğer virtual thread'ler çalışamaz

  4. 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.jar

Pinning 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) <== synchronized

Yaygın Pinning Kaynakları

KaynakÇözüm
synchronized bloğuReentrantLock kullan
synchronized metotReentrantLock 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 olarak synchronized kullanı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)

MetrikPlatform Thread (200)Virtual Thread
Concurrent bağlantı20010,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-preview flag'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 inference

CPU-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=10

10.000 concurrent istek geldiğinde:

  1. 10 virtual thread DB connection alır

  2. 9.990 virtual thread HikariPool.getConnection() çağrısında bekler

  3. Connection timeout (30 saniye) dolunca hata fırlatar

  4. Kullanıcılar Connection is not available hatası 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_count

Ama 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=300000

Migration 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=true

Adı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=50

Adım 6: Pinning Tespiti ile Test Et

# Test ortamında pinning tespiti aç
java -Djdk.tracePinnedThreads=short -jar myapp.jar

Adı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/orders

Migration 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:

  1. Product Service — ürün bilgisi

  2. Review Service — kullanıcı yorumları

  3. 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=200

Sonuç:

  • 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=true

Sonuç:

  • 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. Ama Executors.newVirtualThreadPerTaskExecutor() ile Future kullanı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.jar

Spring 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_file ile virtual thread dump alırken dosya boyutuna dikkat et. 100.000 virtual thread varsa dump dosyası çok büyük olabilir. -format=json ile 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ır

  • I/O-bound iş yükleri için mükemmel (DB, HTTP, dosya I/O). CPU-bound işlerde fayda sağlamaz

  • Pinning tehlikesi: synchronized bloğu virtual thread'i carrier thread'e pinler. ReentrantLock kullan. -Djdk.tracePinnedThreads ile tespit et

  • Connection 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