Virtual Threads

Aykan Ankara
AgeSA İş Teknolojileri
6 min readMay 17, 2024

Merhaba,

Bu yazıda sizlere kısaca Project Loom kapsamında olan (kullanımı kolay, yüksek verimlilik, az maliyetli eş zamanlılık vadeden), Java JDK 19 ile ilk tanıtımı yapılan (pre-release), JDK 21 ile daha olgun bir hale gelen “Virtual Thread” ler ve bununla ilişkili yeni özellikler ile ilgili kısa bir bilgilendirme yapacağım.

Yazıda kullandığım bazı kısaltmalar var. Okuma kolaylığı olması açısından yazının altında açıklamalarını bıraktım.

Kısaca Virtual Thread (VT) (JESP 444), uygulamaları hem performans hem de bakım açısından daha verimli geliştirmek için tasarlanmış bir yapıdır.

Şimdi VT den bahsetmeden önce, JVM de daha önceden mevcut ve VT ler ile ilişkili olan Platform Thread (PT) den kısaca bahsetmem gerekecek. Daha sonra VT leri açıklamaya devam edeceğim.

Platform Thread (PT) nedir?

Platform Thread, işletim sistemine ait olan thread leri (OS Thread) JVM içerisinde sarmalayan yapılardır. PT, sarmaladığı OS Thread (OST) üzerinde Java kodu çalıştırır ve PT nin yaşam döngüsü boyunca ilgili OST yi alıkoyar.

Virtual Thread (VT) nedir?

PT ler gibi, VT ler de “java.lang.Threads” paketinin bir üyesidir. Ancak VT ler, spesifik olarak bir OST ye bağlı değillerdir. VT ler, JVM kontrolündedir. Teknik olarak bir OST üzerinde kodlarını koştukları halde bir farklılık vardır. VT ler, az yer kaplayan ve uygulama işletimlerini bloklamayan (super lightweight) thread lerdir ve PT lerin aksine OS tarafından değil, tamamen JVM tarafından yönetilirler.

VT ler genellikle I/O operasyonlarında çok verimlidirler. Ne zaman VT üzerinde çalışan bir kod bloğu bloklanmaya neden olabilecek bir I/O operasyonu çağırsa Java Runtime çağrıya dönüş alıncaya kadar PT den bu işlemi ayırarak JVM içerisindeki yaşam döngüsünde bekletir. VT ile ilişkili olan PT ise bu sayede diğer VT lerin işletimlerini yapmaya devam eder.

OST, PT ve VT ilişki Diyagramı

PT lerin aksine, VT ler “shallow call stack” yapısına sahiptir (cpu mimarisine özel subroutine yapıların “stack instruction” tarafında “call stack” lerde sadece dönüş adresini -return address- saklar dolayısıyla bu açıdan “lightweight” tabiri kullanılır). PT ler stack de 1 MB yer kaplarlar. VT ler ayrıca thread-local değişkenlerini de destekler.

Az önce de bahsettiğim gibi, VT ler akış bloklamaya müsait olan I/O operasyonlarında kullanılmak için uygun yapılardır. Bunun aksine, uzun süren CPU yoğunluklu işlemler (native methods) için kullanılmaları çok uygun değildir. Çünkü bu işlemler VT leri blokladığı gibi, VT nin bağlı olduğu PT yi de bloklar (örn: fibonacci seri hesaplamaları gibi işlemler). Yani JVM in ilgili VT yi, bloklanmayı engellemek için PT den ayıramaması ve PT nin başka VT ler ile işletime devam etmesini engelleyebilir. (JVM VT Scheduling through PT).

Neden VT Kullanmalıyız?

VT ler yüksek çıktısı olan uygulamalarda — özellikle beklemeye neden olan çok fazla işlemi barındıran uygulamalarda — tercih edilir. Sunucu uygulamaları aslında buna tipik bir örnektir. Çünkü bu uygulamalar çok fazla kaynak tüketimi yapan I/O operasyonlarını içeren istemci taleplerini yönetirler.

Özellikle JDK 19 dan önce senkron programlamada, multi-thread bir uygulama geliştirildiğinde, JVM de yer alan PT ler, OST ler ile eşleşiyordu. İşletim sisteminin, CPU mimarisinin elverdiği biçimde thread havuzu (bundan sonra yazı içerisinde anlam bütünlüğü için “thread pool” olarak ifade edeceğim) limitinde bir kapasiteden yararlanabiliyordu. JDK 19 ile ilk duyurusu yapılan VT ler, JVM Heap içerisinde çoklu olarak PT ler ile eşleşirler ve PT leri taşıyıcı (carrier-thread) olarak kullanırlar. PT ler ise birebir olarak OST ler ile eşleşirler. Bir VT talebi kod tarafında oluşturulduğunda, OST ile eşleşen PT ler bir VT ile eşleşir ve işletim koşulur. Buradaki önemli özelliklerden bir tanesi, VT herhangi bir işlem nedeniyle bloklandığı durumda PT den ayrılır. JVM Heap içerisindeki diğer objeler gibi PT ye bağlı olmadan yaşamına devam eder. PT böylece başka VT ler ile işletimlere devam eder. JVM Heap de geri bildirim bekleyen VT, geri bildirimi elde ettikten sonra tekrar JVM tarafından, PT üzerine alınması sağlanacak şekilde işletimsel olarak planlaması yapılır. (JVM re-scheduling.)

VT ler, PT lerden çok daha geniş bir thread pool a sahiptir. VT ler, OS kernele ait sistem çağrılarına (system call) bağımlı değillerdir. JVM yönetimi sayesinde, OS kernelin thread ler üzerinde yaptıkları işletim geçişi (context switch) özelliğinden de bağımsız hareket ederler. (context switch işlemini JVM yönetir.)

Burada şu bilgiyi de ilave etmek gerek: VT ler, PT lerden hızlı değildir. Sadece bize daha fazla thread kullanma imkanı sunduğundan genişleme (scale) imkanı sağlar. Dolayısıyla hız konusunda bu noktadan katkıları olur.

VT lerin Oluşturulması ve Çalıştırılması

“Thread” ve “Thread.Builder” API ları, PT ve VT oluşturmamız için kullanacağımız paketlerdir. “java.util.concurrent.Executors” yapısı ise yeni oluşturulan VT lerin çalıştırılmasında bize yardımcı olur. “start” metodu VT yi başlatır. “join” metodu ise çağrılan bir VT nin sonlanmasını (terminate) beklememizi sağlar. (klasik thread implementasyonlarında izlenen yolun aynısı kısaca)

“Thread.ofVirtual()” metodu, yeni bir VT “Thread.Builder” oluşumu için bize yardımcı olur.

Aşağıda, temel olarak bir VT nin nasıl oluşturulduğu ve başlatıldığı görülmektedir:

Thread Builder builder = Thread.ofVirtual().name(“ExampleWorkerThread”);

Runnable task = () -> {

System.out.println(“ExampleWorkerThread is running…”);

}

Thread t = builder.start(task);

thread.join();

VT ler aşağıdaki API lar kullanılarak farklı şekillerde oluşturulabilirler:

· Thread.ofVirtual().start(Runnable);

· Thread.ofVirtual().unstarted(Runnable);

· Executors.newVirtualThreadPerTaskExecutor();

· Executors.newThreadPerTaskExecutor(ThreadFactory);

“Thread.Builder.OfPlatform” alt arayüzü PT oluşturulmasını sağlarken (sub-interface), “Thread.Builder.OfVirtual” alt arayüzü VT oluşturulmasını sağlar.

VT lerin Spring Boot da Kullanımı

23 Kasım 2023 tarihinde duyurusu yapılan Spring Boot 3.2.0 ile VT desteği açıklandı. (Şu an için sadece Tomcat ve Jetty uygulama sunucuları tarafından destekleniyor). Varsayılan olarak inaktif gelen bu özellik, deklaratif olarak aktif hale uygulama özellikleri dosyası üzerinden (application.properties/yml) getirilebiliyor:

spring.threads.virtual.enabled=true

Önemli Noktalar

VT lerin, gündemde olan konseptler ile uyumluluğunu görebilmek için bazı kavramlardan kısaca bahsetmek gerekiyor.

Asenkronluk (Asynchronous)

Asenkronluk, eş zamanlılık ile ilişkilidir ancak farklı bir konseptir. Asenkronluk, işletimlerin bloklanmadan ilerlemesidir (non-blocking). Biraz daha açarsak, ilgili işletimin tamamlanmasi için kaynakların (computational resource), istek sahibine (caller) geri bildirimini (response) ileri bir zamanda dönebilme kabiliyetidir.

Reaktiflik (Reactive Programming)

Reaktif programlama kısaca, olay güdümlü (event-driven) bir programlama konseptidir. Olaylar, reaktif hesaplama diyagram modelleri üzerinden işletimlerini gerçekleştirir ve oluşan geri bildirimlere göre akışlarını şekillendirirler. (Farklı reaktif hesaplama diyagram modelleri vardır. örn: Directed Acyclic Graphs, Cycle Graphs vs.)

VT konseptinin temelinde devamlılık (continuation) ve JVM işletim planlama (JVM scheduling) gibi kavramlar olduğundan dolayı, yukarıda bahsettiğimiz konseptleri destekleyen bir yapısı vardır.

Ancak şunu söyleyebiliriz, JVM üzerinde CPU yoğunluklu işlemlerde ve tam reaktif (fully reactive) olarak geliştirilmiş uygulamalarda (“development stack” olarak Spring Boot kapsaminda var olan Spring WebFlux destekli uygulamalar örnek olarak verilebilir) asenkron programlama (ve dolayısıyla bloklanmayan “non-blocking” yapı) kullanıldığı için bu senaryolarda VT ler kullanılabilir. Ancak herhangi bir performans faydası sağlamazlar.

VT lerin gerçek avantajlarını nerede görebiliriz?

Tam Reaktif programlama, temelde JDK 7, NIO.2 i ile gelen Asenkron I/O yapısını kullanır ve bütün istek ve dönüşlerde, ayrı thread mekanizmaları çalıştırılır. Bu programlama konsepti, yüksek ölçeklenme ve bloklanmayan (non-blocking) yapıda olması nedeniyle harika bir yöntem. Ancak bu çözüm aşağıdaki bazı noktalardan biraz komplekstir:

· Uygulamaların kolay bir şekilde tasarlanması, implementasyonu ve debug edilmesi, dolayısıyla kompleks akışların çözümlenmesi ve bakımı,

· Kompleks reaktif akışların (reactive pipelines) uygulanmak zorunda olması,

· Olağandışı durumlar (Exceptions) oluştuğunda gerçek kök nedenin tespit edilmesinde yaşanan zorluklar,

Tam Reaktif yapının çalışma biçimi

İşte VT ler, tam reaktif programlama karşısında, yukarıda bahsettiğimiz temel problemleri giderme konusunda avantaj olarak görünüyor. VT ler sayesinde:

· Semantik değişkenleri barındıran daha açık ve spesifik tanımlanmış (Imperative Style) kodlamaya devam edilebilir,

· Kompleks reaktif akışlara gerek olmaz,

· Bloklanmayan (non blocking) implementasyon modellerine kod bloğu içerisinde direk olarak ihtiyaç olmaz (VT ler JVM üzerinde non-blocking olarak yönetiliyor)

· Uygulama implementasyonu, debug ve hata takibi gibi konularda avantajlı bir kod tasarımı yapmamızı sağlar,

Asenkron bloklama ile VT nin kullanılması

VT ler & Reaktif Programlama

VT ler, yukarıda bahsedildiği gibi, aynı altyapısal şartlarda, (Memory, CPU vs.) uygulama özelinde çok sık kullandığımız bazı işletimlerimizi, tam reaktif konsepte olduğu gibi bloklama yaşamadan ve daha fazla genişleterek (thread scale up) gerçekleştirmemizi sağlar. Ancak bunu yaparken birçok bloklanmayan (non-blocking) işlemi arka planda VT ler ve JVM üstlenir. Bu durum da bize, daha anlaşılır uygulama implementasyonları yapmamızı sağlar. Az kaynak tükettikleri için milyon seviyesinde VT den faydalanabilme imkanı sunar. CPU hassasiyeti olan işletimleri de göz önünde bulundurarak uygulamalarımızı VT lerden faydalanacak şekilde tasarlamak, bize performans konusunda çok yüksek verim kazandırır.

VT ler, şu an için gündemde olan tam reaktif programlama karşısında heyecan verici bir alternatif olarak görünüyor. Önümüzdeki dönemde yaşanacak gelişmeler, VT lerin rolünü nasıl şekillendirecek merakla bekliyorum.

Kısaltmalar:

API: Application Programming Interface

CPU: Central Processing Unit

HTTP: Hypertext Transfer Protocol

I/O: Input / Output

JDBC: Java Data Base Connectivity

JDK: Java Development Kit

JVM: Java Virtual Machine

OS: Operating System

OST: Operating System Thread

PT: Platform Thread

VT: Virtual Thread

Kaynaklar:

[1] https://openjdk.org/jeps/444

[2] https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html

[3] https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html

[4] https://spring.io/blog/2023/11/23/spring-boot-3-2-0-available-now

--

--