CodeGym /Kurslar /JAVA 25 SELF /Yüksək səviyyəli sinxronizatorlar

Yüksək səviyyəli sinxronizatorlar

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

1. CountDownLatch: siqnal üzrə start

Çoxaxınlı dünyada tez-tez bir qrup axının ahəngdar işləməsinə ehtiyac olur — hamı birlikdə başlasın, bitirsin və ya növbəti mərhələyə keçsin. Məsələn:

Yarışı təsəvvür edin. Maşınlar startda dayanıb — kimsə artıq mühərriki qızdırıb, kimsə hələ təkərləri yoxlayır. Amma hakim bayrağı yelləməyənə qədər heç kim yerindən tərpənmir. Bu, koordinasiya məsələsidir.

Başqa bir misal: dostlarla axşam yeməyi hazırlayırsınız — biri tərəvəz doğrayır, biri su qaynadır, biri isə duzun hara yoxa çıxdığını axtarır. Əsas odur ki, bişirməyə başlamazdan əvvəl hamı hazırlığı bitirsin.

Belə hallarda Java bizə hazır sinxronizasiya alətləri verir — təhlükəsiz, anlaşılan və wait()notify() ağrısı olmadan. Ən faydalılardan biri — CountDownLatch. O, sayğac-kilid kimi işləyir: sıfıra enməyənə qədər “qapı” bağlıdır və heç kim irəli getmir. Hamı qeydiyyatdan keçəndə — latch açılır və axınlar sinxron şəkildə işə atılır.

CountDownLatch

CountDownLatch — bu, “bir dəfəlik ventil”dir, bir və ya bir neçə axına digər axınlar müəyyən sayda əməliyyatı bitirənə qədər gözləməyə imkan verir.

Bu, marafon startı kimidir: bütün qaçışçılar xətdə dayanıb start tapançasının atəşini gözləyirlər. Hakim atəş açan kimi (countdown sıfıra çatanda) — hamı qaçır.

Bu ümumiyyətlə necə işləyir

CountDownLatch — axınlar üçün start fitidir. Yaradarkən bir rəqəm verirsiniz — məsələn, 3. Bu, yarış başlamazdan əvvəl almaq lazım olan üç siqnal kimidir.

Startı gözləməli olan axınlar await() çağırır. Onlar xətdə dayanıb hərəkətə hazırdır, amma hələ “əyləci” saxlayırlar. Digər axınlar hazırlığı yerinə yetirərək, hazır olduqca countDown() çağırırlar — sanki siqnal verirlər: “Hazıram!”.

Sayğac sıfıra çatanda — bax! — gözləyən bütün axınlar eyni vaxtda başlayır.

Amma yadınızda saxlayın: CountDownLatch — bir dəfəlik şeydir. Sayğac sıfıra endikdən sonra onu geri qaytarmaq olmaz. Bu, revolver deyil, pirotexniki fişəng kimidir: partladı — və bitdi.

Nümunə: N tapşırığın tamamlanmasını gözləmək

import java.util.concurrent.CountDownLatch;

public class LatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int workers = 3;
        CountDownLatch latch = new CountDownLatch(workers);

        for (int i = 1; i <= workers; i++) {
            int id = i;
            new Thread(() -> {
                System.out.println("İşçi " + id + " işə başladı");
                try { Thread.sleep(500 + id * 200); } catch (InterruptedException ignored) {}
                System.out.println("İşçi " + id + " işi bitirdi");
                latch.countDown(); // sayacı azaldırıq
            }).start();
        }

        System.out.println("Əsas axın bütün işçilərin tamamlanmasını gözləyir...");
        latch.await(); // bütün işçilər bitirənə qədər gözləyirik
        System.out.println("Bütün işçilər bitirdi! Əsas işə davam edirik.");
    }
}

Çıxış:

Əsas axın bütün işçilərin tamamlanmasını gözлəyir...
İşçi 1 işə başladı
İşçi 2 işə başladı
İşçi 3 işə başladı
İşçi 1 işi bitirdi
İşçi 2 işi bitirdi
İşçi 3 işi bitirdi
Bütün işçilər bitirdi! Əsas işə davam edirik.

Nümunə: eyni vaxtda “siqnal” ilə start

CountDownLatch startSignal = new CountDownLatch(1);

for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + " startı gözləyir");
            startSignal.await(); // siqnalı gözləyirik
            System.out.println(Thread.currentThread().getName() + " start verir!");
        } catch (InterruptedException ignored) {}
    }).start();
}

Thread.sleep(1000);
System.out.println("Start siqnalı!");
startSignal.countDown(); // bütün axınlar eyni vaxtda başlayır

2. CyclicBarrier: çoxqatlı fazalar, baryer hərəkətləri

CyclicBarrier: tonqal başında görüş

CyclicBarrier — axınların görüş yeridir. Hər biri öz marşrutu ilə gedir, öz işini görür, sonra isə hamı “baryerdə” toplaşır — sanki dağlarda tonqal başında. Hamı toplaşanda baryer açılır, qrup birlikdə irəli gedir.

CountDownLatch‑dən əsas fərq — bu baryerdən təkrar-təkrar istifadə etmək olar. Hər bir birgə dayanışdan sonra o “yenidən yüklənir” və komanda növbəti mərhələyə keçə bilir.

Təsəvvür edin: Turistlər qrupu uzun marşrutla gedir. Hər kəs öz sürəti ilə hərəkət edir: kimsə kəpənəkləri çəkir, kimsə Wi‑Fi axtarır. Amma hər aşırımda onlar tonqal başında görüşür, bir-birini gözləyir və hara gedəcəklərinə qərar verirlər. Bax bu — CyclicBarrierin işidir.

Necə işləyir

Baryer yaradırsınız və iştirakçıların sayını göstərirsiniz, məsələn, 4. Hər bir axın, nəzarət nöqtəsinə çatanda await() çağırır — və digərlərini gözləyir. Hamı, dördü də toplaşanda, baryer “çıqq” edir və hamını irəli buraxır.

Hətta “baryer hərəkəti” də təyin etmək olar — qrup toplaşanda dəqiq bir dəfə icra olunacaq kod. Məsələn, həmin tonqalın qaladılması və ya log yazmaq: “Mərhələ bitdi, irəli gedirik”. Bunun üçün konstruktora Runnable ötürülür.

Vacib: bir dəfəlik CountDownLatch‑dən fərqli olaraq, CyclicBarrier çoxistifadəlidir. Hər “toplaşmadan” sonra o yenidən növbəti mərhələyə hazır olur — sanki təkrar-təkrar yandırıla bilən əbədi yürüş tonqalı kimi.

Nümunə: fazaların sinxronizasiyası

import java.util.concurrent.CyclicBarrier;

public class BarrierDemo {
    public static void main(String[] args) {
        int parties = 3;
        CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
            System.out.println("Hamı baryerə çatdı! Yeni fazaya başlayırıq.");
        });

        for (int i = 1; i <= parties; i++) {
            int id = i;
            new Thread(() -> {
                try {
                    System.out.println("Axın " + id + " 1-ci fazada işləyir");
                    Thread.sleep(300 + id * 200);
                    System.out.println("Axın " + id + " baryeri gözləyir");
                    barrier.await(); // digərlərini gözləyirik

                    System.out.println("Axın " + id + " 2-ci fazada işləyir");
                    Thread.sleep(200 + id * 100);
                    System.out.println("Axın " + id + " baryeri gözləyir (2)");
                    barrier.await(); // yenə gözləyirik

                    System.out.println("Axın " + id + " işini tamamladı");
                } catch (Exception e) {
                    System.out.println("Xəta: " + e);
                }
            }).start();
        }
    }
}

Çıxış:

Axın 1 1-ci fazada işləyir
Axın 2 1-ci fazada işləyir
Axın 3 1-ci fazada işləyir
Axın 1 baryeri gözləyir
Axın 2 baryeri gözləyir
Axın 3 baryeri gözləyir
Hamı baryerə çatdı! Yeni fazaya başlayırıq.
Axın 1 2-ci fazada işləyir
...

Baryer hərəkəti

CyclicBarrier konstruktoruna bütün axınlar baryerə çatanda bir dəfə icra olunacaq hərəkəti (Runnable) ötürmək olar (məsələn, vəziyyəti yeniləmək, log çıxarmaq).

Tələlər: bəs bir axın “yıxılarsa”?

Əgər axınlardan biri istisna atarsa və ya baryerə çatmasa, digərləri ya sonsuza qədər gözləyəcək, ya da BrokenBarrierException alacaq. Baryer “qırılır” və onu yenidən yaratmaq lazım gəlir.

Bu bölməni “orkestr” xəttinin təbii davamı kimi daha canlı, obrazlı və danışıq tərzində belə yazmaq olar:

3. Phaser: böyük konsertin məharətli dirijoru

Phaser — bir növ “superbaryer”dir. O, CountDownLatchCyclicBarrierin ən yaxşı cəhətlərini birləşdirir, eyni zamanda daha çevikdir. Bu, elə bir orkestrdir ki, musiqiçilər konsertin hissələri arasında gələ və gedə bilirlər, dirijor isə yenə də hər hissənin hamı hazır olanda başlamasına nəzarət edir.

Adi baryerdən fərqli olaraq, Phaser mərhələlərlə işləməyi bacarır — fazalar bir-birini əvəz edir. Kimsə yalnız birinci hissədə ifa edir, kimsə sonra qoşulur, kimsə isə daha tez çıxır — Phaser bunların hamısını sakit qarşılayır.

Necə işləyir

Əvvəlcə Phaser yaradılır, adətən müəyyən sayda iştirakçı ilə — parties. Hər bir axın qeydiyyatdan keçir (register()), öz partiyasını ifa edir və fazanın sonunda arriveAndAwaitAdvance() çağırır — bitirdiyini bildirir və digərlərini gözləyir. Hamı bu nöqtəyə çatanda, Phaser növbəti fazaya keçir və proses təkrarlanır.

İştirakçı artıq lazım deyilsə — arriveAndDeregister() ilə səliqəli şəkildə “təzim edib” səhnəni tərk edə bilər. Yeni iştirakçılar isə əksinə, konsert zamanı birbaşa register() ilə qoşula bilərlər.

Phaser nə zaman Barrier‑dən yaxşıdır

Phaser seçin, əgər proqramınız tək ritmdə deyil, bir neçəsində yaşayırsa:

  • axınların sayı işləmə zamanı dəyişirsə,
  • bir neçə mərhələ varsa və bütün iştirakçıların hamısında iştirak etməsi vacib deyilsə,
  • və ya sadəcə əl ilə sinxronizasiya ilə artıq əlləşmədən maksimum çeviklik istəyirsinizsə.

Əslində, Phaser — dirijordur. O, təkcə çubuqla yönləndirmir, həm də orkestrin tərkibinə, konsertin hissələrinin sayına və hətta kimsə gecikib və ya tez çıxsa belə, uyğunlaşa bilir.

Nümunə: dinamik axın sayı ilə mərhələli emal

import java.util.concurrent.Phaser;

public class PhaserDemo {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(1); // əsas axın

        for (int i = 1; i <= 3; i++) {
            phaser.register(); // iştirakçını qeydiyyatdan keçiririk
            int id = i;
            new Thread(() -> {
                for (int phase = 1; phase <= 2; phase++) {
                    System.out.println("Axın " + id + " " + phase + "-ci fazada işləyir");
                    try { Thread.sleep(200 + id * 100); } catch (InterruptedException ignored) {}
                    phaser.arriveAndAwaitAdvance(); // digərlərini gözləyirik
                }
                System.out.println("Axın " + id + " işini tamamladı");
                phaser.arriveAndDeregister(); // phaser-dən çıxırıq
            }).start();
        }

        // Əsas axın da fazalarda iştirak edir
        for (int phase = 1; phase <= 2; phase++) {
            phaser.arriveAndAwaitAdvance();
            System.out.println("Əsas axın: " + phase + "-ci faza tamamlandı");
        }
        phaser.arriveAndDeregister();
        System.out.println("Bütün fazalar tamamlandı!");
    }
}

Xüsusiyyətlər:

  • İştirakçıları işləmə zamanı əlavə etmək/silmək olar.
  • Cari fazanın nömrəsini bilmək olar: phaser.getPhase().
  • Phaser-i bitirmək olar: phaser.forceTermination().

4. Exchanger: axınlar arasında məlumat dəstələrinin mübadiləsi

Exchanger<T> — iki axın arasında məlumat mübadiləsi üçün sinxronizatordur. Hər bir axın exchange(data) çağırır və hər iki axın görüşəndə, onlar məlumatlarını bir-biri ilə dəyişirlər.

Analogiya: İki kuryer yolayrıcında görüşür və bağlamaları dəyişirlər.

Necə işləyir?

  • Bir axın exchange(data1) çağırır — ikincini gözləyir.
  • İkinci axın exchange(data2) çağırır — hər ikisi bir-birinin məlumatını alır.
  • İkinci axın gəlməzsə — birinci gözləyir (taym‑aut təyin etmək olar).

Nümunə: producer və consumer arasında buferlərin mübadiləsi

import java.util.concurrent.Exchanger;

public class ExchangerDemo {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        // Producer
        new Thread(() -> {
            String data = "Producer-dən məlumatlar";
            try {
                System.out.println("Producer: məlumat göndərir");
                String response = exchanger.exchange(data);
                System.out.println("Producer: cavab aldı: " + response);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        // Consumer
        new Thread(() -> {
            try {
                String received = exchanger.exchange("Consumer-dən cavab");
                System.out.println("Consumer: məlumat aldı: " + received);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Çıxış:

Producer: məlumat göndərir
Consumer: məlumat aldı: Producer-dən məlumatlar
Producer: cavab aldı: Consumer-dən cavab

Tətbiqlər:

  • Axınlar arasında bufer mübadiləsi (məsələn, biri fayldan oxuyur, digəri şəbəkəyə yazır).
  • İki axın arasında fazaların sinxronizasiyası.

5. Təcrübə: paralel konveyer emalı

Tapşırıq: oyun “tik”i (fazalar)

Tutaq ki, bir neçə axınımız var və hər biri oyun dünyasının bir hissəsinə cavabdehdir (məsələn, fizika, AI, renderinq). Hər kəs hər “tik”də (fazada) sinxronlaşmalıdır ki, uyğunsuzluq olmasın.

Həll: CyclicBarrier və ya Phaser istifadə edirik.

import java.util.concurrent.CyclicBarrier;

public class GameTickDemo {
    public static void main(String[] args) {
        int subsystems = 3;
        CyclicBarrier barrier = new CyclicBarrier(subsystems, () -> {
            System.out.println("Bütün altsistemlər tiki tamamladı. Növbətisinə başlayırıq.");
        });

        for (int i = 1; i <= subsystems; i++) {
            int id = i;
            new Thread(() -> {
                for (int tick = 1; tick <= 5; tick++) {
                    System.out.println("Altsistem " + id + " " + tick + "-ci tikdə işləyir");
                    try { Thread.sleep(100 + id * 50); } catch (InterruptedException ignored) {}
                    try {
                        barrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

Tapşırıq: çox sayda işçi üçün “ventil”

Tutaq ki, 100 işçi‑axınımız var və onlar hazırlıqdan sonra eyni anda start verməlidirlər (məsələn, yükləmə testi).

Həll: CountDownLatch istifadə edirik.

import java.util.concurrent.CountDownLatch;

public class MassStartDemo {
    public static void main(String[] args) throws InterruptedException {
        int workers = 100;
        CountDownLatch ready = new CountDownLatch(workers);
        CountDownLatch start = new CountDownLatch(1);

        for (int i = 0; i < workers; i++) {
            new Thread(() -> {
                System.out.println("Axın starta hazırdır");
                ready.countDown(); // hazırlığı bildiririk
                try {
                    start.await(); // ümumi siqnalı gözləyirik
                    System.out.println("Axın start verir!");
                } catch (InterruptedException ignored) {}
            }).start();
        }

        ready.await(); // bütün axınlar hazır olana qədər gözləyirik
        System.out.println("Hamı hazırdır! START!");
        start.countDown(); // start siqnalını veririk
    }
}

6. Sinxronizatorlarla işdə tipik səhvlər

Səhv №1: CountDownLatch‑i çoxistifadəli baryer kimi işlətmək.
CountDownLatch — bir dəfəlikdir! Sıfıra çatdıqdan sonra onu “yenidən yükləmək” olmaz. Təkrarlanan fazalar üçün CyclicBarrier və ya Phaser istifadə edin.

Səhv №2: İstisnalar işlənmir (InterruptedException, BrokenBarrierException).
await() metodları istisna ata bilər — onları həmişə işləyin, əks halda axın “asılı” qala və ya xətayla tamamlana bilər. InterruptedExceptionBrokenBarrierExceptionə diqqət edin.

Səhv №3: Axınlardan biri baryerə çatmadı.
Əgər bir axın “yıxıldı”sa və ya await() çağırmadısa, digərləri sonsuza qədər gözləyəcək (və ya BrokenBarrierException alacaq). Bütün iştirakçıların baryerə çatmasına nəzarət edin.

Səhv №4: Phaser‑də deregister() yaddan çıxıb.
Əgər axın işi bitirib, amma arriveAndDeregister() çağırmayıbsa, Phaser “ölü” iştirakçını gözləyəcək. Axınları həmişə Phaserdən düzgün çıxarın.

Səhv №5: Exchanger‑dən iki axından çoxu üçün istifadə.
Exchanger yalnız iki axın arasında mübadilə üçün işləyir. Axın daha çoxdursa — deadlock alacaqsınız.

Səhv №6: İş prinsiplərini anlamadan müxtəlif sinxronizatorları qarışdırmaq.
Eyni axın qrupunda eyni anda bir neçə fərqli baryer/latch istifadə etməyin — bu, çaşqınlığa və ilişmələrə səbəb ola bilər.

1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Axınların Simfoniyası: Teatr Premerası 🎭
Axınların Simfoniyası: Teatr Premerası 🎭
1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Gizli Mübadilə: "Ani Kuryer" Əməliyyatı 🕵️
Gizli Mübadilə: "Ani Kuryer" Əməliyyatı 🕵️
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION