İçeriğe geç

Virtual Threads vs Platform Threads

İleri 10 dk Çok sık karşılaşılır

30 saniyede özet

Virtual thread daha hızlı değil, beklerken daha ucuz. Veritabanı ya da ağ cevabı beklerken altındaki gerçek thread'i bırakır; kazanç hafiflikten değil, buradan gelir.

Beklemek bedava görünür, ama kimin beklediği önemlidir. Masada dikilen garson başka müşteriye bakamaz.

Aynı garson, iki alışkanlık: masada beklemek ya da siparişi bırakıp yola devam etmek.
Adım adım oku
  1. Klasik modelde garson 1 numaralı masanın yemeği pişene kadar orada bekler.
  2. 2 ve 3 numaralı masalar, garson boşalana kadar sırada kalır.
  3. Virtual thread modelinde garson siparişi mutfağa bırakır ve hemen sonraki masaya geçer.
  4. Tek garson üç masanın siparişini de mutfağa ulaştırdı. Bekleme artık garsonu tutmuyor.
  1. Bayt: Sunucum yoğun saatte tıkandı. İşlemci boş, bellek dolu. Neyi bekliyor bu?

  2. Sen: Thread'lerin hepsi veritabanından cevap bekliyor olabilir mi?

  3. Bayt: Tam olarak. Her biri masanın başında dikilip yemeği bekleyen bir garson gibi.

  4. Bayt: Virtual thread'de garson siparişi mutfağa bırakıp sonraki masaya geçer. Aynı ekip, çok daha fazla masa!

Bir web uygulamasında her gelen istek bir thread alır ve iş bitene kadar bırakmaz. Havuzda 200 thread varsa aynı anda en fazla 200 istek işleyebilirsin.

Sorun şu: bu isteklerin çoğu zamanının büyük kısmını bir veritabanı veya HTTP cevabını bekleyerek geçirir. Thread’lerin çoğu hiçbir iş yapmadan yer tutar.

Havuzu büyütmek neden çözüm değil

Havuzu 200'den 2000 thread'e çıkarsan sorun çözülür mü? Cevabı göster

Bir süre idare eder, sonra iki duvara çarparsın: bellek ve bağlam değiştirme.

  • Bellek. Her platform threadKlasik Java thread'i: bir OS thread'ine birebir bağlıdır ve bloklandığında onu tutar. Yığını sabit boyutludur, bu yüzden sayısı sınırlıdır.Sözlükte gör → bir OS thread’idir ve kendine ~1 MB stack ayırır. 2000 thread ≈ 2 GB, hiçbir iş yapmadan.
  • Geçiş maliyeti. İşletim sistemi binlerce thread arasında sürekli geçiş yapar; bu geçişlerin kendisi işlemci zamanı yer.
Kafam karıştı, daha basit anlat

Her thread kendine büyük bir masa ayırıyor, çoğu zaman da masada oturup cevap bekliyor. Binlerce masa kurmaya kalkarsan salon yetmez.

Hızlı kontrolOrta

Virtual thread'leri havuzda tutmak (`newFixedThreadPool` gibi) iyi bir fikir midir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Virtual thread ne yapıyor

Virtual thread, işletim sisteminin gerçek bir thread’i değildir. Onu JVM yönetir ve az sayıda gerçek thread’in, yani carrier threadBir virtual thread'i o an çalıştıran gerçek OS thread'i. Virtual thread blokladığında taşıyıcı serbest kalır ve başka bir virtual thread'e geçer.Sözlükte gör → thread’lerin (varsayılan olarak çekirdek sayısı kadar) sırtında taşınır.

Kritik davranış:

Virtual thread bir cevap beklemeye başladığında altındaki thread’den iner (unmount): kaldığı yer heap’e not edilir, altındaki thread başka bir işe geçer. Cevap gelince herhangi bir boş thread’e geri biner (mount) ve kaldığı yerden devam eder.

Yani beklemek artık neredeyse bedava. Kod yukarıdan aşağı düz okunur, ama altta kimse boşuna beklemez.

Java 21
// Platform thread: havuz 200 ise eşzamanlılık tavanı 200
var executor = Executors.newFixedThreadPool(200);
// Virtual thread: her görev için bir thread, tavan iş yükü
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (var request : requests) {
executor.submit(() -> {
var user = userClient.fetch(request.userId()); // bloklar ama carrier'ı bırakır
return render(user);
});
}
}

Spring Boot 3.2+ için tek satır yeter:

application.properties
spring.threads.virtual.enabled=true
Kafam karıştı, daha basit anlat

Virtual thread, beklerken masadan kalkan bir çalışan gibidir. Cevap gelince boş bir masaya oturup devam eder. Böylece az masayla çok iş döner.

Hızlı kontrolOrta

Virtual thread ile platform thread arasındaki temel fark nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin ölç

Aşağıdaki simülatörde her kare bir görev. Önce varsayılan ayarla (platform, 8 thread) çalıştır, sonra modeli virtual yap ve aynı iş yükünü izle.

Platform thread vs virtual thread

Tohum 11

Havuzdaki OS thread’leri (0/8)

Görevler · aynı anda uçuşta: 0 · en yüksek: 0

  • sırada: 60
  • CPU: 0
  • I/O bekliyor: 0
  • bitti: 0
Tamamlanan
0/60
En yüksek eşzamanlılık
0
Yaklaşık stack belleği
0 KB
Hız
Adım 0

Şu an ne oldu?

Havuz dolu — 60 görev sırada

8 thread'in hepsi meşgul. I/O bekleyen bir thread hiçbir iş yapmıyor ama yerini de kimseye vermiyor.

Aklında kalsın: Thread-per-request modelinde eşzamanlılık tavanı havuz boyutudur. Havuzu büyütmek çözüm değil: her thread ~1 MB stack ayırır ve bağlam değiştirme maliyeti artar.

Görevler0/2

  • Aynı carrier sayısıyla havuzun 4 katından fazla görevi aynı anda uçuraçık

    İpucu

    Modu virtual thread yap. I/O’da bekleyen bir virtual thread carrier’ı bırakır, o yüzden eşzamanlılık havuz boyutuna bağlı kalmaz.

  • Bir virtual thread’i carrier’a çivile (pinning)açık

    İpucu

    Virtual thread modunda "I/O synchronized içinde" seçeneğini aç. Monitor tutan bir thread I/O’da carrier’ı bırakamaz.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

Sırayla dene:

  • Platform, havuz 8, I/O 12. Aynı anda en fazla 8 görev ilerliyor; geri kalanı sırada bekliyor. “En yüksek eşzamanlılık” değeri havuz boyutunda takılı kalıyor.
  • Modeli virtual yap. Carrier sayısı hâlâ 8, ama uçuştaki görev sayısı çok daha yüksek. Sınır artık havuz değil, iş yükü.
  • I/O süresini 1 yap. İki model neredeyse aynı. Bekleme yoksa kazanacak bir şey de yok.
  • synchronized anahtarını aç. Virtual thread’in avantajı anında kayboluyor.

Simülatörün altındaki Görevler listesi bu iki gözlemi hedefe çeviriyor.

Hızlı kontrolİleri

Her iş yükünü, virtual thread'lerin kazandırıp kazandırmadığına göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Kazandırır

Thread çoğu zaman beklemede

    Kazandırmaz

    Thread çoğu zaman hesap yapıyor

      Pinning: nerede takılır?

      Virtual thread bazı durumlarda carrier’dan inemez. Buna pinningVirtual thread'in taşıyıcısını bırakamadan bloklanması. Olduğunda virtual thread'in tüm kazancı kaybolur — taşıyıcı OS thread'i orada beklemeye başlar.Sözlükte gör → denir ve en sık sebebi synchronized bloğudur:

      Pinning'e yol açan kod
      public class RateLimiter {
      // synchronized içinde bloklayıcı çağrı: virtual thread pinlenir
      public synchronized Response call(Request request) {
      return httpClient.send(request); // carrier boyunca tutulur
      }
      private final ReentrantLock lock = new ReentrantLock();
      public Response call(Request request) {
      lock.lock();
      try {
      return httpClient.send(request); // burada unmount edebilir
      } finally {
      lock.unlock();
      }
      }
      }

      ReentrantLock bu sorunu yaşamaz: kilidi beklerken de, kilidi tutup bir cevap beklerken de virtual thread aşağı inebilir. Kural basit: virtual thread’e geçerken, içinde bekleme olan synchronized bloklarını ReentrantLock’a çevir.

      Diğer pinning sebepleri· istersen atla

      Native metot (JNI) çağrısı içindeyken de pinning olur. Java 21’de -Djdk.tracePinnedThreads=full bunları yakalar.

      JDK 24 (JEP 491) ile synchronized artık pinlemiyor ve bu bayrak kaldırıldı; pinning JFR’ın jdk.VirtualThreadPinned olayıyla izlenir. Java 21’deysen kural geçerli.

      Neyi çözmez? Virtual thread sihirli bir hızlandırıcı değil; tablo nerede işe yaramadığını gösteriyor.

      İş yüküVirtual thread kazandırır mı
      Çok sayıda eşzamanlı HTTP/DB çağrısıEvet — asıl kullanım alanı
      Ağır CPU hesabı (görüntü işleme, şifreleme)Hayır — paralellik hâlâ çekirdek sayısıyla sınırlı
      Az sayıda uzun süren görevHayır — zaten havuz doymuyor
      ThreadLocal’a çok veri koyan kodHayır, zararlı — milyonlarca thread × ThreadLocal = bellek patlaması

      Eşzamanlılık (concurrency) ile paralellik (parallelism) tam burada ayrışır. Virtual thread eşzamanlılığı ucuzlatır; paralellik hâlâ CPU çekirdeği sayısı kadardır.

      Kafam karıştı, daha basit anlat

      synchronized içinde beklerken virtual thread masadan kalkamayabilir, masayı boşuna tutar. Bekleme olan yerlerde ReentrantLock kullan: onunla kalkabilir.

      Hızlı kontrolİleri

      Bir virtual thread'in `synchronized` blok içinde bloklayıcı I/O yapması neden sorundur?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Virtual thread kullanan bir kodda `ThreadLocal` neden dikkat ister?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Yanında gelen: Structured Concurrency

      Java 21’in önizleme özelliği, birden fazla virtual thread’i tek bir kapsamda yönetir:

      StructuredTaskScope
      try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
      var user = scope.fork(() -> userClient.fetch(id));
      var orders = scope.fork(() -> orderClient.fetch(id));
      scope.join().throwIfFailed(); // biri patlarsa diğeri otomatik iptal
      return new Profile(user.get(), orders.get());
      }

      Kazanç: alt görevler kapsam dışına sızamaz, biri başarısız olunca diğerleri iptal edilir ve stack trace anlamlı kalır.

      Aşağıdaki örnek bir bankanın yurt dışı ödemelerinden: her ödeme gönderilmeden önce yaptırım listelerine karşı taranır. İş neredeyse tamamen beklemedir; sınırı veritabanı havuzu koyar.

      Derinleş · Virtual thread'lerle yaptırım taraması: uçtan uca 5 dosya · ~104 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/java/screening/ PaymentScreening.java Her ödeme için bir virtual thread; bloklayan kod olduğu gibi kalır.

      src/main/java/screening/PaymentScreening.java
      // Every outgoing international payment is checked against sanctions lists before
      // it is released. The check is I/O: the payer from our database, the match from
      // the screening provider's HTTP API.
      public class PaymentScreening {
      private final ScreeningClient screening;
      private final CustomerRepository customers;
      public PaymentScreening(ScreeningClient screening, CustomerRepository customers) {
      this.screening = screening;
      this.customers = customers;
      }
      // Plain blocking code. On a virtual thread, each blocking call unmounts the
      // thread and frees its carrier for another task.
      public List<ScreeningResult> screen(List<OutgoingPayment> payments) throws Exception {
      try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
      List<Future<ScreeningResult>> futures = payments.stream()
      .map(payment -> executor.submit(() -> new ScreeningResult(
      payment.id(),
      screening.check(customers.nameOf(payment.customerId()), // blocking JDBC
      payment.beneficiaryName(), // blocking HTTP
      payment.beneficiaryCountry()))))
      .toList();
      List<ScreeningResult> result = new ArrayList<>();
      for (Future<ScreeningResult> future : futures) result.add(future.get());
      return result; // HIT results go to the compliance queue, CLEAR ones are released
      }
      }
      }

      src/main/java/screening/ CustomerRepository.java Virtual thread'ler eşzamanlılığı sınırlamaz. Veritabanını korumak için semaphore.

      src/main/java/screening/CustomerRepository.java
      public class CustomerRepository {
      // 10,000 virtual threads would happily open 10,000 connections.
      // The pool has 20; the semaphore makes the rest wait cheaply instead of timing out.
      private final Semaphore dbPermits = new Semaphore(20);
      private final DataSource dataSource;
      public CustomerRepository(DataSource dataSource) {
      this.dataSource = dataSource;
      }
      public String nameOf(long customerId) throws SQLException, InterruptedException {
      dbPermits.acquire();
      try (var connection = dataSource.getConnection();
      var statement = connection.prepareStatement("SELECT name FROM customers WHERE id = ?")) {
      statement.setLong(1, customerId);
      try (var rs = statement.executeQuery()) {
      return rs.next() ? rs.getString(1) : "?";
      }
      } finally {
      dbPermits.release();
      }
      }
      }

      src/main/java/screening/ TokenCache.java Tarama sağlayıcısının token'ı için ReentrantLock. JDK 24 öncesinde synchronized içinde bloklamak carrier thread'i kilitler.

      src/main/java/screening/TokenCache.java
      public class TokenCache {
      private final ReentrantLock lock = new ReentrantLock();
      private final AuthClient auth;
      private volatile Token token;
      public TokenCache(AuthClient auth) {
      this.auth = auth;
      }
      // The screening provider wants an OAuth token that expires every few minutes.
      // Refreshing blocks on a network call while holding the lock.
      // With `synchronized` (before JDK 24) that pins the carrier thread;
      // ReentrantLock lets the virtual thread unmount while it waits.
      public Token current() {
      Token t = token;
      if (t != null && !t.expired()) return t;
      lock.lock();
      try {
      if (token == null || token.expired()) token = auth.fetchToken();
      return token;
      } finally {
      lock.unlock();
      }
      }
      }

      src/main/resources/ application.yml Spring Boot'ta Tomcat ve @Async'i virtual thread'lere geçiren ayar.

      src/main/resources/application.yml
      spring:
      threads:
      virtual:
      enabled: true # Tomcat request handling, @Async and scheduling run on virtual threads
      datasource:
      hikari:
      maximum-pool-size: 20 # still the real limit on concurrent queries

      src/main/java/screening/ Main.java Çalıştıran program: on bin yurt dışı ödeme, aynı anda en fazla yirmi veritabanı çağrısı.

      src/main/java/screening/Main.java
      public class Main {
      public static void main(String[] args) throws Exception {
      var screening = new PaymentScreening(new FakeScreeningClient(Duration.ofMillis(100)),
      new CustomerRepository(Databases.local()));
      List<OutgoingPayment> payments = IntStream.range(0, 10_000)
      .mapToObj(i -> new OutgoingPayment(i, i % 500, "Beneficiary " + i, "DE"))
      .toList();
      long start = System.nanoTime();
      List<ScreeningResult> result = screening.screen(payments);
      System.out.printf("%d payments screened in %d ms%n", result.size(), (System.nanoTime() - start) / 1_000_000);
      // The time depends on the machine and the database; measure, don't assume.
      }
      }

      Kendini sına

      Önce hızlı bir ısınma: puan yok, kayıt yok.

      Şimşek turu1/5

      Virtual thread'ler hesap ağırlıklı işleri hızlandırır.

      Soru 1/3İleri

      Virtual thread'ler hangi iş yükünde anlamlı kazanç sağlar?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Aklında kalacak üç şey

      1. 1 Virtual thread bir cevap beklerken altındaki gerçek thread'den iner, o thread de başka bir işe geçer. Klasik thread bunu yapamaz.
      2. 2 Sadece hesap yapan işte kazanç yok. Virtual thread aynı anda bekleyebilen iş sayısını artırır, işlemci çekirdeği sayısını değil.
      3. 3 Java 21'de synchronized blok içindeki virtual thread aşağı inemez; buna pinning denir. Bu yüzden synchronized yerine ReentrantLock önerilir.
      Sonraki kapı Kodun, yazdığın sırayla çalıştığından emin misin? İşlemci her zaman o kadar emin değil. Java Memory Model ve Yeniden Sıralama · 10 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı

      Bu dersin üstüne kurulanlar

      Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.