1. Ümumi resurs nədir
Ümumi resurslarla artıq rastlaşmısınız. Ailənin yaşadığı ev — həmin ailənin ümumi resursudur. Ofis soyuducusu — bütün həmkarlar üçün ümumi resursdur. Düşünürəm, ideya aydındır.
Proqramlaşdırmada ümumi resurs — bir neçə axının eyni vaxtda müraciət edə bildiyi dəyişən, obyekt və ya verilənlər strukturudur. Bu, belə ola bilər:
- Emal olunmuş sifarişlərin sayını göstərən sayğac dəyişəni.
- Bəzi axınlar tərəfindən doldurulan, digərləri tərəfindən emal olunan müraciətlər siyahısı.
- Bir neçə axının yazdığı açıq fayl.
- Proqramın müxtəlif hissələrinin işlədiyi verilənlər bazasına qoşulma.
Java-da bir neçə axının müraciət edə biləcəyi istənilən obyekt və ya dəyişən potensial “ümumi resurs”a çevrilir.
Ümumi resurs nümunəsi: qlobal sayğac
public class Counter {
public int value = 0;
}
Əgər bir neçə axın bu sayğacı artıracaqsa, onlar eyni value dəyişəninə müraciət edəcəklər — budur sizə ümumi resurs.
2. Eyni vaxtda giriş problemləri
Təkaxınlı proqramda hər şey sadədir: bir axın — bir icraçı — o, kodun üstündən relslə gedən qatar kimi keçir. Amma işə bir neçə axın qoşulan kimi həqiqi “qılınc rəqsi” başlayır: axınlar ən gözlənilməz yerlərdə bir-birinin işinə qarışa bilər.
Race condition (yarış vəziyyəti)
Race condition — proqramın nəticəsinin axınların hərəkətlərinin necə “qarışmasına” bağlı olduğu vəziyyətdir. Yəni eyni proqramı bir neçə dəfə işə salsanız, nəticə fərqli ola bilər — bu, baq deyil, çoxaxınlılığın “xüsusiyyəti”dir.
Klassik nümunə: iki axın sayğacı artırır
Gəlin sadə bir vəziyyəti modelləşdirək: ümumi sayğacımız var və iki axın onun dəyərini min dəfə artırır.
public class Counter {
public int value = 0;
}
public class CounterDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable incrementTask = () -> {
for (int i = 0; i < 1000; i++) {
counter.value++; // TƏHLÜKƏLİ YER!
}
};
Thread t1 = new Thread(incrementTask);
Thread t2 = new Thread(incrementTask);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Gözlənilən dəyər: 2000");
System.out.println("Faktiki dəyər: " + counter.value);
}
}
Ekranda nə görəcəyik?
Bəzən — 2000, bəzən — 1985, bəzən isə — 1937... Niyə? Çünki counter.value++ əməliyyatı atomik deyil! O, üç addımdan ibarətdir:
- Hazırkı counter.value dəyərini oxumaq.
- Onu 1 vahid artırmaq.
- Geriyə yazmaq.
Əgər iki axın eyni vaxtda eyni dəyəri oxuyub hər ikisi onu artırsalar və sonra nəticəni ayrı-ayrılıqda yazsalar — nəticədə artırmaların biri “itirilə” bilər. Bu, lost update — yeniləmənin itirilməsi deməkdir.
Obyektin qeyri-konsistent vəziyyəti
Əgər bir neçə sahədən ibarət mürəkkəb obyektiniz varsa və axınlar eyni vaxtda müxtəlif sahələri dəyişirsə, obyekt “ qəribə” və ya qeyri-konsistent vəziyyətə düşə bilər. Məsələn, hesabdakı məbləğ azalıb, amma əməliyyatların tarixi yenilənməyib — müştəri təşvişdə, mühasib şokda.
3. Sinxronizasiya niyə lazımdır
Sinxronizasiya — proqramda deməyin yoludur: “Dayan, bu kod hissəsini eyni anda yalnız bir axın icra etməlidir! Qalanlar — gözləsin!” Bu, tualetin qapısındakı “Təmizlik! Daxil olmayın!” lövhəsi kimidir: bir nəfər içəridə olanadək, başqaları çöldə gözləyir (və uzun qalanı ürəklərində danlayırlar).
Məlumatların bütövlüyünə zəmanət
Əgər sayğacımızın həmişə düzgün artırılmasını istəyiriksə, onun dəyərinin bir neçə axın tərəfindən eyni vaxtda dəyişdirilməsini qadağan etməliyik.
Nümunə: sayğacın artırılmasının sinxronizasiyası
public class Counter {
public int value = 0;
public synchronized void increment() {
value++;
}
}
İndi əgər iki axın increment() çağırsa, müəyyən anda yalnız biri bu metodu icra edə biləcək. İkincisi birincisi bitirənə qədər gözləyəcək.
Sxem: sinxronizasiya zamanı nə baş verir
+-------------------+
| Axın 1 | --\
+-------------------+ \
| \
V \
+-------------------+ > [ synchronized increment() ]
| Axın 2 | --/ /
+-------------------+ / /
| / /
V / /
+-------------------+ / /
| Axın 3 | --/ /
+-------------------+ /
| /
V /
+-------------------+ /
| Axın N |/
+-------------------+
Bütün axınlar qorunan kod hissəsini icra etmək üçün növbəyə dayanır. Eyni vaxtda “kritik bölmə”də (synchronized-blokda) yalnız bir axın ola bilər.
4. Sinxronizasiya üsullarına qısa giriş
Java-da sinxronizasiya tək bir üsul deyil, ümumi resursu eyni vaxtda girişdən qorumağa imkan verən bütöv bir “arsenal”dır.
Açar söz synchronized
Bu, Java-da əsas sinxronizasiya alətidir. Onu iki yolla istifadə etmək olar:
Sinxronlaşdırılmış metod
public synchronized void increment() {
value++;
}
Sinxronlaşdırılmış blok
public void increment() {
synchronized (this) {
value++;
}
}
Burada this — kilidləmənin baş verdiyi obyektdir. Bir axın bu bloku icra etdiyi müddətdə, eyni obyektlə eyni bloka daxil olmaq istəyən digər axınlar gözləyəcəklər.
java.util.concurrent-dən ixtisaslaşdırılmış siniflər
- Lock, ReentrantLock — synchronized üçün daha çevik alternativ.
- ReadWriteLock — oxuma və yazma kilidlərinin ayrılması üçün.
- Semaphore — kodu eyni vaxtda icra edən axınların sayını məhdudlaşdırır.
- CountDownLatch, CyclicBarrier və digərləri — axınların işini koordinasiya etmək üçün.
Vacib: Bu gün biz yalnız əsaslarla tanış oluruq — bu siniflər barədə bir az sonra danışacağıq.
5. Praktik nümunə: çoxaxınlı sayğaclı tətbiq
Tutaq ki, hansısa xidmətə istifadəçi müraciətlərinin statistikasını yazırıq. Hər axın — ümumi sayğacı artıran ayrı bir istifadəçidir.
Sinxronizasiya olmadan
public class Counter {
public int value = 0;
}
public class MultiThreadCounterDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable user = () -> {
for (int i = 0; i < 10000; i++) {
counter.value++;
}
};
Thread t1 = new Thread(user);
Thread t2 = new Thread(user);
Thread t3 = new Thread(user);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("Gözlənilən dəyər: 30000");
System.out.println("Faktiki dəyər: " + counter.value);
}
}
Nəticə: Demək olar ki, həmişə 30000-dən az. Bəzən — çox daha az! Niyə? Çünki axınlar bir-birini “üstələyir”.
Sinxronizasiya: səhvi düzəldirik
public class Counter {
public int value = 0;
public synchronized void increment() {
value++;
}
}
public class MultiThreadCounterDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable user = () -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(user);
Thread t2 = new Thread(user);
Thread t3 = new Thread(user);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("Gözlənilən dəyər: 30000");
System.out.println("Faktiki dəyər: " + counter.value);
}
}
Nəticə: Həmişə 30000. Hurra, sinxronizasiya işləyir!
6. Faydalı incəliklər
Vizualizasiya: race condition necə görünür
Gəlin kiçik bir cədvəl çəkək ki, iki axının necə “artırmanı itirə” biləcəyini göstərək:
| Addım | Axın 1 | Axın 2 | value dəyəri |
|---|---|---|---|
| 1 | value=0 oxuyur | 0 | |
| 2 | value=0 oxuyur | 0 | |
| 3 | 1-ə qədər artırır | 0 | |
| 4 | 1-ə qədər artırır | 0 | |
| 5 | 1 yazır | 1 | |
| 6 | 1 yazır | 1 |
Sinxronizasiya nə zaman lazımdır
Sinxronizasiya həmişə lazım deyil. Dəyişən öz kiçik dünyasında yaşayır və onunla yalnız bir axın işləyirsə — rahat ola bilərsiniz. Amma onu başqa axınlarla paylaşan kimi, sinxronizasiyasız mümkün deyil. Hətta hər şeyin yaxşı olacağını düşünsəniz belə — inanmayın. Yarış səhvi məkrlidir: uzun müddət gizlənə bilər, sonra isə ən uyğunsuz anda qəfil üzə çıxar.
Gələcək üçün baza: başqa hansı sinxronizasiya vasitələri var
Bu gün yalnız əsas alətlə — synchronized — tanış olduq. Növbəti mühazirələrdə baxacağıq:
- Obyekt monitoru necə işləyir və hansı kilid növləri var.
- Statik sinxronlaşdırılmış metodlar nədir (static + synchronized).
- volatile açar sözü necə işləyir və nə üçün lazımdır.
- Sinxronizasiya üçün müasir siniflər hansılardır (Lock, Semaphore və s.).
7. Ümumi resurslarla işləyərkən tipik səhvlər
Səhv № 1: Çoxaxınlılığı nəzərə almamaq.
Ən çox yayılan səhvlərdən biri — dəyişənə bir neçə axının giriş edə biləcəyini nəzərə almamaqdır. Hətta proqram hazırda təkaxınlı olsa belə, sonradan kimsə axınlar əlavə edəcək — və baqlar “heç yerdən” peyda olacaq.
Səhv № 2: Yetərsiz və ya həddən artıq sinxronizasiya.
Əgər ümumi resursa girişi sinxronlaşdırmasanız — race condition və qeyri-konsistent məlumatlar əldə edəcəksiniz. Əgər hər şeyi ard-arda sinxronlaşdırsanız, proqram kilidlərdən “boğulacaq” və yavaşlayacaq. Həmişə yalnız həqiqətən lazım olanı sinxronlaşdırmağa çalışın.
Səhv № 3: Yanlış obyekt üzərində sinxronizasiya.
Əgər girişi müxtəlif obyektlər üzərində sinxronlaşdırsanız (məsələn, lokal dəyişənlər və ya sətir literalları üzərində), bu, ümumi resursu qorumayacaq. Bütün axınlar eyni obyekt üzərində sinxronlaşdırılmalıdır.
Səhv № 4: Atomik olmayan əməliyyatlardan atomiklik gözləmək.
i++ əməliyyatı atomik deyil! Hətta dəyişən volatile kimi elan edilsə belə, bu, inkrementi atomik etmir. Belə əməliyyatlar üçün sinxronizasiya lazımdır.
Səhv № 5: “Mənim şansım gətirdi, məndə hər şey işləyir”.
Race condition sizin kompüterinizdə üzə çıxmaya bilər, amma mütləq serverdə və ya istifadəçidə özünü göstərəcək. Heç vaxt çoxaxınlı proqramlarda “bəxtə” arxayın olmayın!
GO TO FULL VERSION