1. Qısa olaraq əsas olan
Artıq java.util.concurrent paketindəki siniflərlə, xüsusən də ExecutorService ilə bir az tanışsınız. Bu, sanki “tapşırıq meneceri”dir: siz ona işi göndərirsiniz (məsələn, submit() vasitəsilə), o isə hansı thread-lə və nə vaxt icra olunacağına özü qərar verir. Adətən “pərdə arxasında” sabit ölçülü thread pool işləyir, bu da resurslara qənaət edir və hər tapşırıq üçün yeni thread yaratmır.
Lakin virtual thread-lərlə hər şey dəyişir! İndi özünüzə hər tapşırığa — ayrıca thread “lüksünü” rəva görə bilərsiniz və JVM-in resurslarının tükənməsindən qorxmazsınız.
Yeni üsul: Executors.newVirtualThreadPerTaskExecutor()
Java 21-də hər tapşırığı ayrıca virtual thread-də işə salan ExecutorService yaratmağın yeni yolu peyda oldu:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Vacib fərq:
- Köhnə thread pool-lar (Executors.newFixedThreadPool, Executors.newCachedThreadPool) OS thread-lərinin “bahalığı” səbəbindən eyni vaxtda icra olunan tapşırıqların sayını məhdudlaşdırırdı.
- Yeni virtual Executor demək olar məhdudiyyətsizdir: hər tapşırıq üçün öz yüngül virtual thread.
Sadə nümunə
10 tapşırığı virtual Executor-a göndərək:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualExecutorDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 1; i <= 10; i++) {
int taskId = i; // lambda üçün dəyişəni tuturuq
executor.submit(() -> {
System.out.println("Task " + taskId + " is running in thread: " +
Thread.currentThread());
});
}
executor.shutdown();
}
}
Nə baş verir?
Hər tapşırıq öz virtual thread-ində işə düşəcək və siz bu kimi sətirlər görəcəksiniz:
Task 1 is running in thread: VirtualThread[#24]/runnable@ForkJoinPool-1-worker-1
...
2. Kütləvi paralellik: minlərlə tapşırıq — problem deyil!
Virtual thread-lərin gücünü hiss etmək üçün ExecutorService-ə 10 yox, məsələn, 100_000 tapşırıq göndərək. Klassik pool-larda bu, fili soyuducuya yerləşdirməyə bənzəyərdi: JVM tez yaddaşı tükədər və ya ciddi şəkildə ləngiyərdi. Virtual thread-lərlə isə hər şey fərqlidir!
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualExecutorMassiveDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 1; i <= 100_000; i++) {
int taskId = i;
executor.submit(() -> {
// Nümunə üçün — sadəcə 1 ms yatırıq
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// System.out.println("Task " + taskId + " done."); // Çap etmirik, əks halda çox sətir olacaq!
});
}
executor.shutdown();
}
}
Diqqət: ekrana 100_000 sətir çıxarmaq pis fikirdir: konsol virtual thread-lərdən daha tez “tıxanacaq”. Yaxşısı budur ki, ya ümumiyyətlə konsola yazmayın, ya da yalnız ilk bir neçə tapşırığı göstərin.
3. newVirtualThreadPerTaskExecutor necə işləyir
Qısa desək: bu ExecutorService siz ona göndərdiyiniz hər tapşırıq üçün yeni virtual thread yaradır. Sabit ölçülü pool-dan fərqli olaraq, burada tapşırıq növbəsi və eyni vaxtda işləyən thread-lərin sərt məhdudiyyəti yoxdur (JVM və avadanlıq limitləriniz istisna olmaqla).
Arxitektura baxımından:
- Virtual thread-lər azsaylı real OS (carrier) thread-lərinə “map olunur”.
- JVM hansı virtual thread-in nə vaxt işə düşəcəyinə, dayandırılıb bərpa olunacağına özü qərar verir.
- Əgər thread bloklanırsa (məsələn, fayl oxumaqda və ya şəbəkə gözləməsində), JVM virtual thread-i “dondurub” carrier-thread-i digər tapşırıqlar üçün azad edə bilər.
4. Nümunə: nəticələrin Future ilə işlənməsi
Əgər tapşırıq nəticə qaytarırsa, ExecutorService Future tipində obyekt qaytarır. Hər şey adi thread-lərdəki kimidir:
import java.util.concurrent.*;
public class VirtualExecutorWithResult {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(500);
return "Hello from virtual thread!";
});
System.out.println("Result: " + future.get()); // Nəticəni gözləyirik
executor.shutdown();
}
}
Hər şey tanışdır: dəyər qaytaran tapşırıqları göndərə, nəticəni get() ilə gözləyə bilərsiniz, istisnalar isə standart qaydada işlənir.
5. Executor-u düzgün necə bağlamaq
Proqramın “asılı” qalmaması üçün ExecutorService-i bağlamağı unutmaq olmaz (thread-lər virtual olsa belə).
shutdown() və awaitTermination
executor.shutdown(); // Deyirik: artıq tapşırıq qəbul etmirik
executor.awaitTermination(1, TimeUnit.MINUTES); // Bütün tapşırıqların bitməsini gözləyirik (maksimum 1 dəqiqə)
Bu niyə vacibdir?
Əgər shutdown() çağırılmasa, virtual thread-lər yaşamağa davam edə bilər və main() bitdikdən sonra belə proqram bağlanmaya bilər. Bu, yeni başlayanların tipik səhvidir.
6. Faydalı nüanslar
Müqayisə: virtual Executor vs klassik thread pool
| Klassik pool (newFixedThreadPool) | Virtual Executor (newVirtualThreadPerTaskExecutor) | |
|---|---|---|
| Thread-lərin sayı | Pool-un ölçüsü ilə məhdud | Hər tapşırığa bir virtual thread, demək olar məhdudiyyətsiz |
| Növbədə tapşırıqlar | Bəli, bütün thread-lər məşğul olduqda | Adətən yoxdur: tapşırıq dərhal thread alır |
| Thread-in dəyəri | Yüksək (stack, OS resursları) | Çox aşağı (planlaşdırma JVM tərəfindən) |
| Miqyaslana bilmə | Məhduddur | Demək olar məhdud deyil |
| Nələr üçün uyğundur | CPU-bound tapşırıqlar, məhdud paralellik | I/O-bound tapşırıqlar, kütləvi paralellizm |
Veb serverlərlə inteqrasiya
Müasir veb serverlər (məsələn, Tomcat, Jetty, Undertow) artıq virtual thread-ləri dəstəkləməyə başlayır. Bu o deməkdir ki, hər HTTP sorğunu ayrı virtual thread-də emal etmək olar və istifadəçi axını zamanı “boğulmaqdan” qorxmazsınız.
Üstünlük: callback-lər və CompletableFuture ilə mürəkkəb asinxron sxemlər qurmağa ehtiyac yoxdur; kod sadələşir — adi bloklayan kod yaza bilərsiniz, lakin tətbiq yenə də miqyaslanır.
Kütləvi test və yükün imitasiya edilməsi
Virtual thread-lər minlərlə eyni vaxtlı istifadəçi, sorğu və ya əməliyyatı “simulyasiya” etmək lazım olan testlər üçün əladır. Məsələn, serverə 10_000 paralel sorğu göndərən test, hər biri öz virtual thread-ində.
Faylların və şəbəkə bağlantılarının paralel emalı
Əgər tətbiq çoxlu fayl və ya şəbəkə bağlantısı ilə işləyirsə, hər bağlantını ayrıca virtual thread-də emal etmək olar, pool-ları əl ilə idarə etməyə ehtiyac olmadan.
7. Virtual Executor-larla işləyərkən tipik səhvlər
Xəta №1: shutdown() çağırmağı unutmaq. Əgər Executor-u bağlamasanız, proqram tamamlanmayacaq — virtual thread-lər hələ də yeni tapşırıqları gözləyəcəklər. Lazım olduqda awaitTermination(...) əlavə edin.
Xəta №2: ağır hesablamalar üçün virtual thread-lərdən istifadə. Virtual thread-lər CPU-ya tam yük verən tapşırıqları sürətləndirmir. CPU-bound üçün sabit pool-u (Executors.newFixedThreadPool) istifadə etmək və ölçünü diqqətlə seçmək daha yaxşıdır.
Xəta №3: tapşırıq daxilində istisnaları görməməzlikdən gəlmək. Əgər tapşırıq istisna atsa, o, əsas thread-ə düşməyəcək — Future vasitəsilə ( get() metodu) və ya lambda daxilində try/catch ilə işləyin.
Xəta №4: köhnə və yeni sintaksisi/JDK versiyasını qarışdırmaq. Düzgün JDK versiyasından (Java 21+) istifadə etdiyinizi və IDE-nin virtual thread-ləri dəstəkləmək üçün qurulduğunu yoxlayın. Konkret metod — Executors.newVirtualThreadPerTaskExecutor().
Xəta №5: kontekst ötürmək üçün ThreadLocal-a arxalanmaq. Virtual thread-lər tez-tez yaradılıb məhv olunur; ThreadLocal gözlənildiyi kimi davranmaya bilər. Kontekst ötürmək üçün ScopedValue-dan istifadə edin (Scoped Values; ətraflı — növbəti mühazirədə).
GO TO FULL VERSION