CodeGym /Kurslar /JAVA 25 SELF /Scoped Values və axınların yeni mexanikaları (Java 21+)

Scoped Values və axınların yeni mexanikaları (Java 21+)

JAVA 25 SELF
Səviyyə , Dərs
Mövcuddur

1. Niyə ThreadLocal aktuallığını itirir

Ümumiyyətlə ThreadLocal nəyə lazımdır?

Klassik çoxaxınlılıqda, axınlar uzunömürlü olanda (məsələn, serverdə), bəzən hər bir axın üçün ayrıca saxlanmalı olan və başqaları ilə qarışmamalı məlumatlara ehtiyac olur. Məsələn, istifadəçi adı, sorğunun ID-si və ya müvəqqəti bufer.

Bunu etmək üçün Java-da ThreadLocal<T> yarandı — axının “şəxsi məkanı”, burada məlumatları qonşulara mane olmadan saxlamaq olar:

ThreadLocal<String> user = new ThreadLocal<>();

user.set("Alice"); // dəyər yalnız bu axın üçün saxlanılır
String name = user.get(); // məhz burada "Alice" qaytaracaq, digər axınlarda — null

Niyə ThreadLocal virtual axınlarla uyğun gəlmir

Virtual axınlar “ağır” köhnə axınlardan tam fərqli yaşayır. Onlar minlərlə yaradılıb yox olur — bəzən millisaniyənin hissələrində. ThreadLocal isə məlumatı sanki axın həmişə yaşayacaqmış kimi konkret axına bağlayır.

Virtual axın işini bitirdikdə, ThreadLocal-dakı məlumatları yaddaşda asılı qala bilər — hətta axının özü çoxdan bitmiş olsa belə. Bu, sızmalara gətirib çıxarır, çünki JVM həmin dəyərlərin artıq heç kimə lazım olmadığını həmişə bilmir.

Əgər axınlar yenidən istifadə olunursa (məsələn, hovuzlarda), daha xoşagəlməz vəziyyət mümkündür: “yad” kontekst təsadüfən yeni sorğuya keçə bilər. Təsəvvür edin, istifadəçi Petya Vasya-nın məlumatlarını alır — və salam, buglar və zəifliklər.

ThreadLocal az sayda və uzunömürlü axınlarda özünü əla göstərir. Amma virtual axınlarla — bu, elə bil hər saniyə yox olan şkafda əşyaları saxlamağa cəhddir.

2. Scoped Values: konteksti ötürməyin yeni üsulu

Scoped Values — Java 21-dən gələn yeni alətdir və ThreadLocal problemini daha zərif şəkildə həll edir. ThreadLocal-da olduğu kimi məlumatı axının içində saxlamaq əvəzinə, o, məlumatı icra sahəsinə “bağlayır” — yəni kodun konkret hissəsinə. Dəyər yalnız həmin hissə icra olunduğu müddətdə yaşayır və sonra avtomatik yox olur, yaddaşda iz buraxmır.

import java.lang.ScopedValue;

ScopedValue<String> USER = ScopedValue.newInstance();

ScopedValue.where(USER, "Alice").run(() -> {
    System.out.println("Hello, " + USER.get()); // Çıxış: Hello, Alice
});

Kod run blokundan kənara çıxan kimi dəyər artıq əlçatan olmur — ona müraciət cəhdi istisna atacaq. Heç nəyi əl ilə təmizləmək lazım deyil.

Scoped Values yaddaşı çirkləndirmir, konteksti axınlar arasında qarışdırmır və elə daxili sahələr yaratmağa imkan verir ki, orada daxili dəyərlər müvəqqəti olaraq xariciləri üstələyə bilir. Bu, xüsusən virtual axınlar dünyasında konteksti ötürməyin səliqəli, proqnozlaşdırılan və təhlükəsiz yoludur.

3. Scoped Values istifadəsinə nümunələr

Nümunə 1: İstifadəçi kontekstinin ötürülməsi

Tutaq ki, müxtəlif istifadəçilərdən gələn sorğuları emal edən bir serverimiz var. Hər bir sorğu üçün kimin başladığını bilmək istəyirik.

import java.lang.ScopedValue;

public class ServerExample {
    static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) {
        processRequest("Alice");
        processRequest("Bob");
    }

    static void processRequest(String userName) {
        ScopedValue.where(USER, userName).run(() -> {
            handleBusinessLogic();
        });
    }

    static void handleBusinessLogic() {
        System.out.println("İstifadəçi üçün emal edirik: " + USER.get());
    }
}

Nə baş verəcək:

  • Hər bir sorğu üçün öz scope-u yaradılır, burada USER “Alice” və ya “Bob” olur.
  • handleBusinessLogic() daxilində həmişə düzgün istifadəçi adını alacağıq.
  • Sorğunun emalı bitən kimi dəyər yox olur.

Nümunə 2: Kontekstlə loglama

Tutaq ki, sorğu identifikatorunu avtomatik loglara əlavə etmək istəyirik:

import java.lang.ScopedValue;

public class LoggingExample {
    static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            String reqId = "REQ-" + i;
            ScopedValue.where(REQUEST_ID, reqId).run(() -> {
                log("Emalın başlanğıcı");
                doWork();
                log("Emalın sonu");
            });
        }
    }

    static void log(String message) {
        System.out.printf("[%s] %s%n", REQUEST_ID.get(), message);
    }

    static void doWork() {
        log("İşləyirik...");
    }
}

Nəticə (nümunə):

[REQ-1] Emalın başlanğıcı
[REQ-1] İşləyirik...
[REQ-1] Emalın sonu
[REQ-2] Emalın başlanğıcı
[REQ-2] İşləyirik...
[REQ-2] Emalın sonu
[REQ-3] Emalın başlanğıcı
[REQ-3] İşləyirik...
[REQ-3] Emalın sonu

Hər bir scope öz sorğu identifikatorunu saxlayır və axınlar arasında heç bir qarışıqlıq mümkün deyil.

4. Scoped Values və virtual axınlar: ideal cütlük

Niyə Scoped Values virtual axınlarla xüsusilə faydalıdır

Virtual axınlar qısamüddətlidir — onlar minlərlə yaradılıb məhv edilir, bəzən saniyənin hissələrində. Buna görə də məlumatın sərt şəkildə axına “bağlandığı” köhnə ThreadLocal yanaşması burada işləmir: axınlar çox tez yox olur, kontekst isə təsadüfən sızıb qarışa bilər.

ScopedValue isə əksinə, məlumatı birbaşa tapşırığa — onun icra sahəsinə bağlayır. Bu o deməkdir ki, kontekst (məsələn, istifadəçi adı və ya sorğu ID-si) axını deyil, kodu izləyir. Tapşırıq bitdikdə dəyər avtomatik yox olur. Virtual axınlar üçün bu, ideal həlldir: təhlükəsiz, təmiz və sürprizsiz.

Nümunə: Virtual axınlarla kütləvi tapşırıq emalı

import java.lang.ScopedValue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadScopedValueDemo {
    static final ScopedValue<Integer> TASK_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 10_000; i++) {
            int taskId = i;
            executor.submit(() -> ScopedValue.where(TASK_ID, taskId).run(() -> {
                processTask();
            }));
        }

        executor.shutdown();
    }

    static void processTask() {
        // Hər bir tapşırıq üçün öz TASK_ID
        System.out.println("Tapşırıq işlənir #" + TASK_ID.get());
    }
}

Əsas məqamlar:

  • Hər bir tapşırıq üçün TASK_ID dəyəri ilə öz scope-u yaradılır.
  • Tapşırıqlar paralel icra olunsa belə, dəyərlər axınlar arasında qarışmır.
  • Yaddaş sızması yoxdur: scope tapşırıqla birlikdə “ölür”.

5. Müqayisə: ThreadLocal vs ScopedValue

Meyar ThreadLocal ScopedValue
Bağlanma Axına Kod sahəsinə (scope)
Həyat dövrü Axın yaşadığı müddətdə Scope icra olunduğu müddətdə
Təhlükəsizlik Sızma və qarışıqlıq riski Sızma yoxdur, qarışıqlıq yoxdur
Virtual axınlar Qeyri-effektiv, risklidir İdeal uyğun gəlir
İstifadə
set/get
where(...).run(...), get
Yerləşmə Override dəstəkləmir Dəyərləri müvəqqəti üstələmək olar

6. Daxili sahələr (scopes): dəyərlərin üstələnməsi

ScopedValue<String> INFO = ScopedValue.newInstance();

ScopedValue.where(INFO, "Xarici").run(() -> {
    System.out.println(INFO.get()); // "Xarici"
    ScopedValue.where(INFO, "Daxili").run(() -> {
        System.out.println(INFO.get()); // "Daxili"
    });
    System.out.println(INFO.get()); // "Xarici"
});

Nəticə:

Xarici
Daxili
Xarici

Bu, məsələn, bir tapşırığın daxilində konteksti müvəqqəti yenidən təyin etmək lazım olanda rahatdır.

Scoped Values: tipik istifadə ssenariləri

  • İstifadəçi və ya sorğu identifikatorunun ötürülməsi: hərəkətləri loglamaq və ya hüquqları yoxlamaq üçün.
  • Loglama: kontekstin avtomatik loglara əlavə edilməsi.
  • Traslama: debug və profilləmə üçün.
  • Tranzaksiya parametrləri: məsələn, izolasiya səviyyəsi və ya iş rejimi.
  • Yalnız bir tapşırığın (və ya onun sub-tapşırıqlarının) daxilində görünən istənilən “kontekst”.

7. Digər yeni mexanikalar: Structured Concurrency

Structured Concurrency — bu yanaşmada əlaqəli tapşırıqlar (məsələn, bir əməliyyatın alt-prosesləri) vahid bütöv kimi idarə olunur: əgər valideyn tapşırıq bitirsə və ya yıxılsa, bütün uşaq tapşırıqlar avtomatik ləğv edilir. Bu, “unudulmuş” və ya “asılı qalan” axınların riskini azaldır.

Nümunə (çox sxematik):

try (var scope = StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> result1 = scope.fork(() -> fetchData1());
    Future<String> result2 = scope.fork(() -> fetchData2());

    scope.join(); // hər ikisinin tamamlanmasını gözləyirik
    scope.throwIfFailed(); // əgər ən az biri uğursuz oldu — istisna atırıq

    String combined = result1.resultNow() + result2.resultNow();
    System.out.println(combined);
}

Üstünlüklər:

  • Tapşırıqların həyat dövrünün daha səliqəli idarə olunması.
  • “Asılı qalan” alt-proseslər yoxdur.
  • Xətaları idarə etmək daha asandır.

Structured Concurrency hələlik preview rejimindədir, amma artıq aktiv şəkildə inkişaf etdirilir.

8. Praktik məsləhətlər və məhdudiyyətlər

Scoped Values nə vaxt istifadə olunmalıdır?

  • Konteksti tapşırıqlar arasında ötürmək lazım olduqda, xüsusən virtual axınlarla işləyərkən.
  • Əvvəllər ThreadLocal istifadə etmisinizsə — ScopedValue-a keçməyin daha yaxşı olub-olmadığını düşünün.

ThreadLocal nə vaxt hələ də lazımdır?

  • Nadir hallarda, axın çox uzun yaşayırsa və kontekst onun bütün həyat dövrü boyu “daimi” olmalıdırsa (məsələn, legacy-kodla işləyərkən).

Məhdudiyyətlər

  • Scoped Values scope yaradıldıqdan sonra dəyişdirilə bilməz — onlar “yalnız oxu” üçün nəzərdə tutulub.
  • Scoped Values scope-dan kənarda istifadə edilə bilməz: sahədən kənarda dəyər alma cəhdi istisna atacaq.
  • Scoped Values böyük obyektləri saxlamaq üçün istifadə etməyin — sahə yüngül və sürətli olmalıdır.

9. Scoped Values istifadə edərkən tipik səhvlər

Səhv № 1: dəyəri scope-dan kənarda almağa cəhd. Əgər ScopedValue.where(...) blokundan kənarda USER.get() çağırsanız, NoSuchElementException istisnası alacaqsınız. Əmin olun ki, müraciət yalnız sahə daxilində baş verir.

Səhv № 2: dəyəri scope daxilində dəyişməyə cəhd. Scoped Values — dəyişməz konteynerdir. Dəyəri müvəqqəti “yenidən təyin” etmək lazımdırsa, daxili scope yaradın.

Səhv № 3: ThreadLocal və ScopedValue birlikdə istifadə olunur. Zəruri olmayınca bu mexanikaları qarışdırmayın — bu, kontekstdə qarışıqlığa və səhvlərə səbəb ola bilər.

Səhv № 4: məntiqi run() blokuna salmağı unutmusunuz. Əgər ScopedValue.where(USER, "Alice") yazıb .run(() -> { ... }) əlavə etməsəniz, heç bir scope yaradılmayacaq!

Səhv № 5: Scoped Value-i uzunömürlü qlobal məlumat üçün istifadə etməyə cəhd. Belə məqsədlər üçün daha yaxşısı adi dəyişənlərdən və ya (əsaslandırılıbsa) ThreadLocal-dan istifadə etməkdir.

1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
Virtual Threads
Virtual Threads
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION