1. Unudulan unlock/release: diqqətsizlər üçün tələ
ReentrantLock və ya Semaphore kimi müasir sinxronizasiya alətlərindən istifadə edərkən ən məkrli səhvlərdən biri — unlock() və ya release() çağırmağı unutmaqdır. Kilidi azad etməsəniz, digər axınlar onun açılmasını... sonsuzadək gözləyəcək. Proqram donacaq və siz uzun müddət ekrana baxıb nəyin baş verdiyini anlamağa çalışacaqsınız.
ReentrantLock ilə nümunəyə baxaq:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
// Ah! unlock() unuduldu — indi hamı ilişəcək!
count++;
}
}
Hər şey zərərsiz görünür, amma müxtəlif axınlardan increment() bir neçə dəfə çağırılsa, ilk çağırışdan sonra qalan axınlar kilidin açılmasını sonsuzadək gözləyəcək.
Bu vəziyyətdən qaçmaq üçün try-finally konstruksiyasından istifadə edin:
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
İndi, metodun ortasında istisna baş versə belə, kilid zəmanətli şəkildə azad olunacaq.
Bu, sanki kimsə sanitar qovşağı tutub (içəridən qapanıb), sonra qapını açmağı unudub və pəncərədən çıxıb. Qalanlar isə həmin adam çıxanadək gözləyəcəklər... Belə etməyin!
2. Yanlış obyekt üzərində sinxronizasiya: “Ah, kilidi səhv yerə asmışam!”
Java-da synchronized açar sözü müəyyən bir obyektə girişə kilid qoyur. Ancaq kilidləmə üçün yanlış obyekt seçsəniz, sinxronizasiya gözlədiyiniz kimi işləməyəcək.
Səhv №1: lokal dəyişən üzərində sinxronizasiya
public void doSomething() {
Object lock = new Object();
synchronized (lock) {
// Hər dəfə yeni obyekt — heç bir sinxronizasiya yoxdur!
// Axınlar bir-birini gözləmir.
// Kritik bölmə qorunmur!
}
}
Burada hər bir axın öz lock obyektini yaradır. Nəticədə real kilidləmə baş vermir — axınlar eyni anda kritik bölmədən keçirlər.
Düzgün:
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// İndi bütün axınlar eyni lock obyektindən istifadə edir
// və həqiqətən bir-birini gözləyir.
}
}
Səhv №2: sətir literalı üzərində sinxronizasiya
public void doSomething() {
synchronized ("lock") {
// Sətir literalları intern edilir: proqramın müxtəlif hissələri
// təsadüfən eyni sətir üzərində sinxronizasiya edə bilər!
}
}
Nəticə:
Yalnız privat, bu məqsəd üçün xüsusi yaradılmış və başqa yerdə istifadə olunmayan obyektlər üzərində sinxronizasiya edin.
3. İkili bloklanma (deadlock): “Sən mənə — mən sənə, və hər ikisi ilişdi”
Deadlock (qarşılıqlı bloklanma) — klassik hadisədir. İki (və ya daha çox) axın ardıcıl olaraq müxtəlif kilidləri tutur və bir-birini gözləyir, nəticədə proqram tamamilə ilişib qalır.
Nümunə:
public class DeadlockExample {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized (lockA) {
// Təcrübənin “təmizliyi” üçün bir az gözləyək
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (lockB) {
// ...
}
}
}
public void method2() {
synchronized (lockB) {
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (lockA) {
// ...
}
}
}
}
Əgər bir axın method1(), digəri isə — method2() çağırsa, birincisi lockA-nı tutacaq və lockB-ni gözləyəcək, ikincisi isə əksinə. Nəticədə hər ikisi sonsuza qədər bir-birini gözləyəcək.
Necə qaçmaq olar?
- Bütün axınlarda kilidləri həmişə eyni ardıcıllıqla götürün.
- Eyni vaxtda saxlanan kilidlərin sayını minimuma endirin.
- Proqram donub qalıbsa, diaqnostika alətlərindən istifadə edin (məsələn, jstack).
Analogiya:
Bu, sanki iki nəfər dar dəhlizdə qarşılaşıb, hər biri yolu verəcəyini, amma yalnız o biri əvvəlcə kənara çəkilərsə, deyir. Nəticədə hər ikisi dayanıb qalır və kimsə birinci geri çəkilənə qədər gözləyirlər.
4. Həddindən artıq sinxronizasiya: “Artıq ehtiyat, az ehtiyatdan yaxşıdır?” — hər zaman deyil!
Bəzən tərtibatçılar, səhvlərdən ehtiyatlanaraq, hər şeyi sinxronizasiya edirlər. Nəticədə məhsuldarlıq düşür, fayda isə sıfır.
Nümunə:
public synchronized void add(int value) {
// Burada yalnız bir sətir var və sinxronizasiyaya ehtiyac yoxdur!
System.out.println("Əlavə edildi: " + value);
}
Bu halda sinxronizasiya lazım deyil: System.out.println vasitəsilə ekrana çıxış artıq axın təhlükəsizdir, həm də metod ümumi resurslarla işləməz.
Bu nə vaxt kritik olur?
Əgər tez-tez çağırılan və qorunmaya ehtiyacı olmayan metodları sinxronizasiya etsəniz, proqramın məhsuldarlığını kəskin şəkildə azaldacaqsınız. Axınlar növbəyə düzülür, halbuki paralel işləyə bilərdilər.
Ən yaxşı təcrübə:
Yalnız həqiqətən gərəkli olanı sinxronizasiya edin. Kritik bölmə mümkün qədər kiçik olmalıdır.
5. volatile-in səhv istifadəsi: “Görünürlük var, atomiklik yoxdur!”
Java-da volatile modifikatoru dəyişənin dəyişikliklərinin bütün axınlara görünəcəyini təmin edir. Amma o, atomikliyi təmin etmir.
Səhv:
private volatile int counter = 0;
public void increment() {
counter++; // Atomik deyil!
}
counter++ əməliyyatı dəyərin oxunması, artırılması və geri yazılmasından ibarətdir. İki axın eyni anda bu kodu icra edərsə, yekun dəyər gözləniləndən kiçik ola bilər.
Düzgün:
Atomik əməliyyatlar üçün synchronized, AtomicInteger və ya digər axın təhlükəsiz siniflərdən istifadə edin.
import java.util.concurrent.atomic.AtomicInteger;
private final AtomicInteger counter = new AtomicInteger();
public void increment() {
counter.incrementAndGet();
}
volatile nə vaxt istifadə olunur?
Sadə bayraqlar üçün (məsələn, “işi dayandır”), atomiklik tələb olunmadıqda.
GO TO FULL VERSION