1. Klassik thread-lər: necə işləyir və harada problem yaranır
Gəlin əvvəlcə Java-da adi thread-lərin (onlara platforma və ya native thread-lər də deyilir) necə işlədiyini xatırlayaq. Məhz o thread-lər ki, new Thread(...) ilə yaradılır.
Siz new Thread(() -> { ... }).start(); çağıranda, JVM sadəcə kod parçasını işə salmır. O, əməliyyat sistemindən həqiqi icra thread-i yaratmağı xahiş edir. OS onun üçün ayrıca stack ayırır (adətən bir neçə meqabayt) və digər xidmət resurslarını rezerv edir.
Belə bir thread tapşırığı icra olunduğu müddətdə yaşayır və bu vaxt ərzində əməliyyat sisteminin thread cədvəlində yer tutur. Belə thread-lər nə qədər çox olsa, onların stack-lərinə bir o qədər yaddaş sərf olunur və OS-ə düşən yük artır. Məhz buna görə eyni anda çoxlu thread işləyəndə tətbiq “boğula” bilər — sistem onların arasında kontekst keçidinə həddən artıq vaxt sərf edir.
Nümunə: klassik thread
Thread thread = new Thread(() -> {
System.out.println("Thread-dən salam!");
});
thread.start();
Sadə görünür, düzdür? Amma bir-iki deyil, məsələn, on min belə thread yaradın — proqramınız tezliklə boğulmağa başlayacaq. Ya yaddaş bitəcək, ya da sistem thread limitinin tükəndiyini deyəcək. Bu Java-nın xətası deyil, arxitekturanın təbii nəticəsidir: thread-lər ağır və bahalı resurslardır.
Niyə belə olur? Hər thread öz stack-ını alır (adətən 1–2 meqabayt), üstəlik əməliyyat sistemi tərəfindən bütöv bir xidmət strukturları dəsti. Üstəlik, OS-ın özü də on minlərlə thread təqdim olunanda sevinmir — onun məhdudiyyətləri var və onlar kifayət qədər sərt ola bilər.
Hətta yaddaş tükənməsə belə, başqa bir problem yaranır — kontekstin dəyişdirilməsi. Thread-lər həddən artıq çox olanda, sistem onların arasında daim atlanır, vəziyyətlərini saxlayıb bərpa edir. Bütün bunlar vaxt aparır və performansı yeyir, ona görə də “kütləvi paralelləşdirmə”dən qazanc alınmır.
“Bir thread — bir sorğu” problemi
Köhnə server tətbiqlərində (məsələn, Tomcat və ya Jetty-də) tez-tez “thread‑per‑request” modeli istifadə olunurdu: hər daxil olan istifadəçi sorğusu üçün ayrıca thread ayrılırdı. Bu rahatdır, amma əgər 10_000 istifadəçiniz varsa, sizə 10_000 thread lazımdır! Server üçün çətinləşir və yarış yaddaş uğrunda gedir, sorğuların emal sürəti uğrunda yox.
Nəticə:
Klassik thread-lər az sayda paralel tapşırıqlar üçün yaxşıdır, amma on minlərlə və yüz minlərlə miqyasa qədər skalalanmır.
2. Virtual thread-lər (Virtual Threads) nədir?
Məhz burada bugünkü mühazirənin qəhrəmanı — virtual thread-lər — ortaya çıxır. Bu sadəcə “daha bir thread” deyil, tam başqa arxitektura ideyasıdır.
Virtual thread-lər — əməliyyat sistemi deyil, bilavasitə JVM tərəfindən idarə olunan thread-lərdir. Onlar tamamilə Java daxilində reallaşdırılıb və yaddaşı “şişirtmədən” və ləngimələr yaratmadan çox böyük sayda (on minlərlə, yüz minlərlə) yaradıla bilirlər.
Qısa olaraq:
- Platform Thread (platforma thread-i): OS thread-i ilə birbaşa uyğun gələn adi thread.
- Virtual Thread (virtual thread): OS deyil, JVM tərəfindən idarə olunan yüngül thread.
Bu necə qurulub?
Virtual thread-lər — OS-də yox, JVM-in özündə yaşayan “yüngül” thread-lərdir. Onlar platform threads adlanan kiçik bir real thread hovuzunun üzərində işləyir. Təsəvvür edin ki, JVM onları orkestrin dirijoru kimi idarə edir: məhdud sayda musiqiçi (real thread-lər) var, amma dirijor onların arasında partiyaları (virtual thread-ləri) mahircasına bölüşdürür.
Arxitektura sxemi:
+-------------------+ +-------------------+
| Virtual Thread 1 |---\ | Platform Thread |
| Virtual Thread 2 |---->====> | (Carrier Thread) |
| Virtual Thread 3 |---/ +-------------------+
... (Operating system)
Carrier Thread — JVM-in bir çox virtual thread-i icra etdiyi adi OS thread-idır. Əgər hansısa virtual thread qəfil bloklanarsa — məsələn, diskdən və ya şəbəkədən məlumat gözləyirsə — JVM onu sadəcə “dondurur” və carrier thread-i başqa tapşırıqlar üçün azad edir.
Niyə bu inqilabidir?
Çünki indi adi, xətti kod yaza bilərsiniz — sonsuz callback-lər, CompletableFuture və “cəhənnəmvari” thenApply zəncirləri olmadan — və bununla yanaşı tətbiqi minlərlə eyni vaxtlı əməliyyata qədər miqyaslaya bilərsiniz.
Virtual thread-lər cəmi onlarla kilobayt yer tutur (adi thread-lərdəki meqabaytların əvəzinə) və demək olar ki, ani şəkildə yaradılır. Buna görə onları minlərlə başladıb dayandırmaq olar, OS-in onların ağırlığından çökməsindən qorxmadan. Bu, Java-da paralel proqramlaşdırmanı nəhayət yüngül və təbii edir.
3. Virtual thread-lərin üstünlükləri
Skalabilik
Virtual thread-lərlə siz on minlərlə, yüz minlərlə paralel tapşırığı işlətmək lüksünə sahibsiniz. Məsələn, hər şəbəkə sorğusunu ayrı thread-də emal etmək — və serverin “partlayacağından” narahat olmamaq.
Göstəriş: 100 000 virtual thread
for (int i = 0; i < 100_000; i++) {
Thread.ofVirtual().start(() -> {
// Burada istənilən məntiq ola bilər
try {
Thread.sleep(1000); // İşin imitasiya edilməsi
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
System.out.println("Bütün thread-lər işə salındı!");
Bu kod adi noutbukda asanlıqla icra olunur!
Eyni şeyi adi thread-lərlə etməyə çalışın — ya OutOfMemoryError görəcəksiniz, ya da kompüteriniz “kərpic”ə çevriləcək.
Proqramlaşdırma sadəliyi
Virtual thread-lər sizə tanış “bloklayan” kodu yazmağa imkan verir, onu asinxron çağırış “spagetti”sinə çevirmədən. Məsələn, Thread.sleep, InputStream.read, Socket.accept kimi çağırışlardan rahatlıqla istifadə edə bilərsiniz — JVM bütün carrier thread-i bloklamamaq üçün özü qayğı göstərəcək.
Oxunaqlığın və dəstəyin yaxşılaşması
Çətin callback sxemləri və CompletableFuture əvəzinə siz xətti, anlaşılan kod yazırsınız. Bu, bug sayını azaldır və dəstəyi asanlaşdırır.
Velosiped ixtira etməyə ehtiyac yoxdur
Əvvəllər minlərlə sorğunu paralel emal etmək üçün asinxron framework-lər, reaktiv kitabxanalar (Netty, Vert.x, Project Reactor) tələb olunurdu və onlar xüsusi proqramlaşdırma üslubu tələb edirdi. İndi onlarsız da keçinmək mümkündür — və yenə də skalabilik əldə etmək olar.
4. Arxitektura: virtual thread-lər “kapot altında” necə işləyir
Carrier thread-lərə xəritələmə
JVM kiçik bir real thread hovuzu yaradır (carrier threads) — adətən onların sayı prosessor nüvələrinin sayı qədər olur. Bütün virtual thread-lər bu carrier thread-lərdə “səyahət edir”, sanki avtobuslardakı sərnişinlər kimi.
- Virtual thread bloklananda (məsələn, şəbəkədən cavab gözləyəndə), JVM onu carrier thread-dən “çıxarıb” növbəyə qoyur.
- Thread işini davam etdirə bilən kimi, JVM onu yenidən boş carrier thread-ə “oturdur”.
Bənzətmə:
Təsəvvür edin ki, sizdə 4 taksi (carrier threads) var və 10 000 müştəriyə (virtual threads) xidmət göstərirsiniz. Nə vaxtsa bir müştəri ünvana çatıb düşən kimi, taksi dərhal növbətini götürür. Heç kim boş dayanmaz və taksilər sərnişinlərin ağırlığından “sınmaz”.
Planlaşdırma və keçid
JVM hazırda hansı virtual thread-in icra olunacağını özü qərar verir. Əgər thread I/O-da bloklanarsa, o, digər thread-lərin işləməsinə mane olmur.
5. Virtual thread-lərin məhdudiyyətləri və xüsusiyyətləri
Virtual olan hər şey qızıl deyil
Uzunmüddətli hesablamalar üçün deyil: Əgər sizdə prosessoru daima məşğul edən tapşırıq varsa (ağır CPU‑bound), virtual thread performans artımı verməyəcək. Sadəcə ona görə ki, carrier threads yenə də nüvələrin sayı ilə məhdudlaşır.
Bəzi bloklamalar səmərəsizdir: Köhnə sinxronizasiya mexanizmləri (məsələn, synchronized vasitəsilə və native mutex-lərlə) JVM-ə virtual thread-i “dondurmağa” imkan verməyə bilər. Belə hallarda carrier thread virtual thread ilə birlikdə gözləyəcək və skalabilik azalacaq.
Bütün kitabxanalar virtual thread-lərlə dost deyil: Əgər kitabxana native çağırışlar edir və ya spesifik bloklamalardan istifadə edirsə, virtual thread-lər gözlənildiyi kimi davranmaya bilər.
Nümunə: Virtual Threads-i nə vaxt istifadə etməmək lazımdır
Əgər sizdə sonsuz dövrdə işləyən və rəqəmləri hesablayan tapşırıq varsa, virtual thread heç bir üstünlük verməyəcək. Yenə də nüvələrin sayına dirənəcəyik.
Thread.ofVirtual().start(() -> {
while (true) {
// Sonsuzadək sayırıq
}
});
Nəticə:
Bir carrier thread bu virtual thread-lə məşğul olacaq və digər tapşırıqlar öz növbələrini gözləyəcəklər.
6. Müqayisə: Platform Thread vs Virtual Thread
| Xüsusiyyət | Platform Thread (Adi) | Virtual Thread (Virtual) |
|---|---|---|
| İdarə olunur | OS tərəfindən | JVM |
| Bir thread üçün yaddaş | Meqabaytlar | Onlarla kilobayt |
| Thread sayı | Adətən < 10_000 | Minlərlə, yüz minlərlə |
| Yaradılma dəyəri | Bahalı | Ucuz |
| Skalabilik | Məhduddur | Demək olar ki, məhdud deyil |
| Nə üçün uyğundur | Uzunmüddətli, CPU‑bound | Qısa, I/O‑bound tapşırıqlar |
| Keçidlərin idarəsi | OS | JVM |
| Uyğunluq | 100% | Demək olar ki, həmişə, amma nüanslar var |
7. Nümunə: Virtual Threads-dən əvvəl və sonra server necə görünərdi
Əvvəl (Platform Threads)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
new Thread(() -> handleClient(client)).start();
}
Problem:
5 000 qoşulmadan sonra server “boğulmağa” başlayacaq.
Sonra (Virtual Threads, Java 21+)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
Thread.ofVirtual().start(() -> handleClient(client));
}
Sehr:
İndi on minlərlə bağlantını emal etmək olar — və thread limitləri barədə düşünmədən!
9. Virtual thread-lərə keçiddə tipik səhvlər
Səhv №1: Hesablama yönümlü tapşırıqlarda sürətlənmə gözləmək. Virtual thread-lər prosessoru tam yükləyən tapşırıqları sürətləndirmir. Belə tapşırıqlar üçün yenə də nüvələrin sayına dirənirik.
Səhv №2: Köhnə bloklayan sinxronizasiyalardan istifadə. Əgər köhnə bloklamalardan istifadə edirsinizsə (məsələn, native şəkildə “kilidlənə” bilən obyektlərdə synchronized), virtual thread-lər carrier thread-dən “çıxarıla” bilməyə bilər və üstünlüklər itirilər.
Səhv №3: Üçüncü tərəf kitabxanalarının davranışını nəzərə almamaq. Bəzi üçüncü tərəf kitabxanalar virtual thread-lərlə işləməyə hazır olmaya bilər (məsələn, JNI və ya native bloklamalardan istifadə edirlərsə).
Səhv №4: Sehrli performans artımı gözləmək. Virtual thread-lər panacea deyil. Onlar hər şeyi sürətləndirmir, yalnız I/O‑bound tapşırıqlar üçün paralelliyi ucuz və rahat edir.
GO TO FULL VERSION