1. Eşzamanlı programlamanın temeli: thread, process ve çalışma birimi
Program dediğimiz şey aslında bilgisayara verilen işlerin toplamı. Mesela bir web sunucusu aynı anda kullanıcıdan istek alır, veritabanına gider, dosya okur, başka bir servise HTTP isteği atar ve cevap üretir. Thread, bu işlerden birini yürüten çalışma hattıdır.
Basit düşünelim: Bir restoran var. Sipariş almak, yemeği hazırlamak ve hesabı kapatmak farklı işler. Tek çalışan varsa herkes sırada bekler. Birden fazla çalışan varsa işler paralel yürür. Thread de program içindeki çalışan gibidir.
Process
Uygulamanın kendisi. Belleği, kaynakları ve çalışma alanı vardır.
Thread
Process içindeki çalışma yolu. Aynı process içinde birden fazla thread olabilir.
Thread, programın aynı anda birden fazla işi yürütmesini sağlayan çalışma birimidir.
2. Java'da platform thread modeli: Thread sınıfı, lifecycle ve thread pool kullanımı
Java'da yıllardır kullandığımız klasik thread modeli platform thread modelidir. Platform thread, Java tarafındaki `Thread` nesnesinin işletim sistemi thread'iyle eşleşmesi demektir. Yani pahalı bir kaynaktır: stack belleği ayırır, oluşturması maliyetlidir ve sayısı sınırlıdır.
public class PlatformThreadExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("Bu iş ayrı bir platform thread üzerinde çalışır");
});
thread.start();
thread.join();
}
}
Daha gerçek hayatta her istek için elle `new Thread()` açmayız. Bunun yerine thread pool kullanırız. Pool, sınırlı sayıda thread tutar ve gelen işleri bu thread'lere dağıtır.
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
try (var executor = Executors.newFixedThreadPool(100)) {
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
callDatabase();
return "ok";
});
}
}
}
}
Thread pool'daki 100 thread'in tamamı veritabanı cevabı bekliyorsa, CPU boş olsa bile yeni işler sırada kalır. Çünkü thread'ler çalışmıyor gibi görünse de bekleme sırasında tutulur.
3. Virtual thread modeli: JVM tarafından yönetilen hafif thread yaklaşımı
Virtual thread, Java'nın JDK tarafında yönettiği hafif thread modelidir. Kod yazarken yine thread gibi düşünürsün: iş başlar, bekler, devam eder, biter. Fakat işletim sistemi tarafında her virtual thread için ayrı bir OS thread tutulmaz.
Bu özellikle backend için büyük farktır. Çünkü backend işlerinin çoğu CPU hesabı değil, I/O bekleme işidir: database, HTTP, dosya, queue, cache. Virtual thread beklemeye girince JVM onu askıya alabilir ve alttaki gerçek thread'i başka işe verebilir.
Virtual thread çalışma akışı
Request gelir
Virtual thread açılır
DB beklenir
Carrier boşa çıkar
Cevap dönülür
import java.time.Duration;
import java.util.concurrent.Executors;
public class VirtualThreadExecutorExample {
public static void main(String[] args) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofMillis(200));
return "ok";
});
}
}
}
}
public class SingleVirtualThreadExample {
public static void main(String[] args) throws InterruptedException {
Thread worker = Thread.startVirtualThread(() -> {
System.out.println("Bu kod virtual thread üzerinde çalışıyor");
});
worker.join();
}
}
Platform thread ile virtual thread arasındaki davranış farkı
- Platform thread: OS thread'e bağlıdır, pahalıdır, sayısı sınırlıdır.
- Virtual thread: JVM tarafından yönetilir, hafiftir, çok yüksek sayılara çıkabilir.
- Platform thread: uzun CPU işleri için hâlâ doğrudur.
- Virtual thread: çok sayıda beklemeli backend işi için çok uygundur.
4. Carrier thread, mounting/unmounting ve pinning probleminin mekanizması
Virtual thread kendi başına işlemcide koşmaz. Onu gerçekten çalıştıran alttaki platform thread'e carrier thread denir. JVM, çok sayıda virtual thread'i daha az sayıda carrier thread üstünde taşır.
Normal durumda virtual thread I/O beklerken carrier thread'i bırakır. Böylece carrier başka virtual thread'i çalıştırabilir. Ama bazı durumlarda virtual thread carrier'ı bırakamaz. Buna pinning denir.
İyi durum ve kötü durum
import java.time.Duration;
class SlowService {
synchronized String load() throws InterruptedException {
// Java 21'de virtual thread burada beklerse carrier thread'i de tutabilir.
Thread.sleep(Duration.ofSeconds(2));
return "done";
}
}
8 carrier thread olduğunu düşün. 8 virtual thread synchronized blok içinde beklerken carrier'ları bırakmazsa, sistemde binlerce virtual thread olsa bile çalıştıracak carrier kalmayabilir. Sonuç: throughput düşer, latency artar, bazı senaryolarda açlık veya kilitlenmeye benzeyen davranışlar görülür.
5. Java 21 ve Java 24 karşılaştırması: final virtual thread desteğinden synchronized pinning iyileştirmesine
Java 21, virtual thread'leri final hale getirdi. Yani artık preview değil, gerçek production özelliği. Fakat Java 21'de önemli bir dikkat noktası vardı: virtual thread, `synchronized` method/blok içinde bloklanırsa carrier thread'e pinned kalabiliyordu.
Java 24 ile gelen JEP 491 bu konuda büyük bir iyileştirme yaptı: virtual thread'lerin monitor yani `synchronized` kullanımında carrier'a pinned kalmadan çalışabilmesi hedeflendi. Bu, eski Java kodlarını virtual thread ile kullanırken önemli bir rahatlama sağlar.
Java 21
- Virtual thread final özelliktir.
- `synchronized` içinde bloklama pinning oluşturabilir.
- Uzun bekleme varsa carrier thread tutulabilir.
- Yoğun trafikte performans kaybı görülebilir.
Java 24
- JEP 491 ile synchronized pinning sorunu iyileştirilir.
- Monitor kullanımı virtual thread'lerle daha uyumlu hale gelir.
- Kod değişmeden daha iyi ölçeklenme alınabilir.
- Native/foreign çağrılar hâlâ dikkat ister.
Java 24 "artık her şey sınırsız hızlı" demek değildir. CPU-bound işleri virtual thread hızlandırmaz. Virtual thread'in güçlü olduğu yer, çok sayıda beklemeli I/O işidir.
Performans kaybını nasıl okumak gerekir?
Java 21 ile Java 24 arasındaki farkı "Java 21 yavaş, Java 24 hızlı" diye okumak doğru olmaz. Doğru okuma şudur: Eğer uygulamada virtual thread'ler sık sık `synchronized` içinde bloklanıyorsa, Java 21'de carrier thread'ler tutulabilir ve concurrency düşer. Java 24 bu özel durumda daha iyi davranır. Ancak uygulama zaten synchronized içinde beklemiyorsa veya iş CPU-bound ise fark sınırlı olabilir.
6. Semaphore kullanımı: virtual thread çokluğunu dış kaynak kapasitesiyle dengelemek
Virtual thread çok hafif diye her şeyi sınırsız açmak iyi fikir değildir. Veritabanı connection pool'un 50 ise, aynı anda 10.000 sorguyu database'e yığmak sistemi boğar. Burada Semaphore kullanarak aynı anda kaç işin kritik kaynağa gireceğini sınırlarsın.
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class VirtualThreadSemaphoreExample {
public static void main(String[] args) {
var databaseLimit = new Semaphore(50);
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
databaseLimit.acquire();
try {
return queryDatabase();
} finally {
databaseLimit.release();
}
});
}
}
}
}
Buradaki fikir şu: 10.000 virtual thread açabilirsin, ama aynı anda sadece 50 tanesi database'e gider. Diğerleri bekler. Virtual thread beklemeyi ucuz hale getirir; semaphore ise dış kaynakları korur.
Java 21'de sorun daha çok `synchronized`/monitor pinning tarafındadır. `Semaphore` gibi `java.util.concurrent` araçları virtual thread'lerle daha uygun bir bekleme modeli sunar. Yine de limitleri doğru seçmezsen sistem yavaşlar; çünkü dış kaynak zaten sınırlıdır.
7. Mini demo: pinning ve limitler performansı nasıl etkiler?
Bu gerçek benchmark değil; kavramı zihinde oturtmak için basit bir model. İş sayısını, bekleme süresini ve Java 21'deki pinning oranını değiştir. Java 24 tarafında synchronized pinning etkisini azaltılmış kabul ediyoruz.
Virtual thread simülatörü
Sonuç
Virtual thread, Java backend tarafında "okunabilir kod + yüksek concurrency" dengesini çok güçlendirdi. Java 21 ile production hayatına girdi; Java 24 ile synchronized kaynaklı carrier pinning problemi önemli ölçüde azaltıldı. Pratikte iyi kullanım şudur: I/O ağırlıklı işlerde virtual thread kullan, dış kaynakları semaphore veya pool limitleriyle koru, CPU-bound işleri ayrı ele al ve production'da JFR/observability ile pinning davranışını izle.