1. Deadlock-a dərindən baxış
Deadlock (“qarşılıqlı bloklama”) — bu, iki və ya daha çox axının bir-birini sonsuzadək gözlədiyi vəziyyətdir: hər biri hansısa resursu saxlayır və yanındakı axının artıq tutduğu başqa bir resursu almağa çalışır. Nəticədə heç kim işi davam etdirə bilmir və proqram “donur”. Bu, dar körpüdə burun-buruna dayanmış iki maşına bənzəyir: biri geri getməyənə qədər — heç kim keçə bilməz.
Java-da deadlock — istisna deyil (Exception), bu, proqramın “donmasıdır”. Axınlar nə tamamlanır, nə də səhvlə yıxılır — sadəcə bir-birini sonsuzadək gözləyirlər. Buna görə belə xətalar hiyləgərcəsinədir: yalnız “müəyyən şəraitdə” üzə çıxırlar.
Deadlock-un yaranması üçün dörd şərt
- 1. Qarşılıqlı istisna (Mutual Exclusion)
Resurs eyni anda yalnız bir axın tərəfindən ələ keçirilə bilər (məsələn, obyekt-monitor, fayl). - 2. Saxlama və gözləmə (Hold and Wait)
Axın bir resursu saxlayır və birincini buraxmadan ikinciyi almağa çalışır. - 3. Preemption yoxdur (No Preemption)
Resursu axından “zorla almaq” olmaz: onu yalnız axının özü azad edə bilər. - 4. Dövrəvi gözləmə (Circular Wait)
Qapalı bir zəncir var, burada hər axın zəncirdə növbəti tərəfindən saxlanılan resursu gözləyir.
Deadlock yalnız bu şərtlərin HAMISI eyni vaxtda yerinə yetiriləndə mümkündür. Onlardan ən az birini pozmaq kifayətdir — və qarşılıqlı bloklama mümkün olmayacaq.
2. Deadlock ilə kod nümunəsi
Sxem
- İki resurs var: lock1 və lock2 (adi obyektlər).
- Axın A əvvəlcə lock1-i ələ keçirir, sonra lock2-ni almağa çalışır.
- Axın B əvvəlcə lock2-ni ələ keçirir, sonra lock1-i almağa çalışır.
Nümunə (bunu öz kodunuzda etməyin!)
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// Axın 1
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Axın 1: lock1-i ələ keçirdi");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Axın 1: lock2-ni ələ keçirməyə çalışır");
synchronized (lock2) {
System.out.println("Axın 1: lock2-ni ələ keçirdi");
}
}
});
// Axın 2
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Axın 2: lock2-ni ələ keçirdi");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Axın 2: lock1-i ələ keçirməyə çalışır");
synchronized (lock1) {
System.out.println("Axın 2: lock1-i ələ keçirdi");
}
}
});
thread1.start();
thread2.start();
}
}
İşə salanda nə baş verəcək?
- Axın 1 lock1-i, axın 2 — lock2-ni ələ keçirir.
- Hər ikisi “yuxuya gedir” 100 millisaniyə, birinci kilidi ələ keçirməyə macal tapırlar.
- Sonra hər biri ikinci kilidi ələ keçirməyə çalışır, amma o, artıq digər axın tərəfindən saxlanılır.
- Hər iki axın bir-birini sonsuzadək gözləyir — deadlock yaranır.
Konsolda görəcəksiniz:
Axın 1: lock1-i ələ keçirdi
Axın 2: lock2-ni ələ keçirdi
Axın 1: lock2-ni ələ keçirməyə çalışır
Axın 2: lock1-i ələ keçirməyə çalışır
Niyə deadlock yaranır? Ətraflı izah
- Qarşılıqlı istisna: hər bir kilidi eyni anda yalnız bir axın saxlaya bilər.
- Saxlama və gözləmə: Axın 1 lock1-i saxlayır və lock2-ni gözləyir; Axın 2 lock2-ni saxlayır və lock1-i gözləyir.
- Preemption yoxdur: kilidi kənardan “götürmək” mümkün deyil — yalnız synchronized blokundan çıxanda azad olunur.
- Dövrəvi gözləmə: gözləmə iki axın arasında dairəvi hala gəlib.
3. Deadlock-u necə qaçınmaq və qarşısını almaq
Resursları eyni ardıcıllıqla ələ keçirmək
Əsas qayda: bir neçə axın bir neçə resursu ələ keçirməli olduqda — bunu həmişə eyni ardıcıllıqla edin. Məsələn: hamısı üçün əvvəl lock1, sonra lock2.
public void doSomething() {
Object firstLock = lock1;
Object secondLock = lock2; // vahid ardıcıllıq "lock1 -> lock2"
synchronized (firstLock) {
synchronized (secondLock) {
// Hər iki resursla iş
}
}
}
Vahid ardıcıllıq olduqda dairəvi gözləmə mümkün deyil — deadlock üçün şərtlərdən biri pozulur.
Taymautla tryLock istifadəsi (ReentrantLock)
Əvvəlcədən hansı resursların lazım olacağı məlum deyilsə, ReentrantLock və tryLock metodundan istifadə edin ki, sonsuzadək gözləməyəsiniz.
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockDemo {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public void doWork() {
try {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// Kritik bölmə
} finally {
lock2.unlock();
}
} else {
System.out.println("lock2-ni ələ keçirmək mümkün olmadı, əməliyyatı geri alırıq");
}
} finally {
lock1.unlock();
}
} else {
System.out.println("lock1-i ələ keçirmək mümkün olmadı, əməliyyatı geri alırıq");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Üstünlük: əməliyyatı geri ala və sonra yenidən cəhd edə bilərsiniz. Mənfi cəhət: kod daha mürəkkəbdir, amma deadlock istisna olunur.
Kilidləri saxlama müddətini minimuma endirin
Kilidi mümkün qədər az saxlayın. synchronized/lock daxilində yalnız zəruri olanı edin; qalan hər şeyi kritik bölmədən kənara çıxarın.
İç-içə kilidləmələrdən qaçının
İç-içə synchronized/lock nə qədər az olsa, qarşılıqlı bloklama riski bir o qədər aşağıdır. Mümkündürsə — əlaqəli resurslar üçün tək kiliddən istifadə edin.
Hazır thread-safe strukturlardan istifadə edin
Standart kitabxana bir çox məsələni artıq həll edib: ConcurrentHashMap kimi kolleksiyalar və java.util.concurrent paketindəki digər siniflər deadlock riskini minimuma endirir və kodu sadələşdirir.
4. Deadlock-un diaqnostikası
Thread Dump və jstack
Thread Dump — JVM-də bütün axınların vəziyyətinin şəklidir.
Konsoldan əldə etmə:
jstack <pid>
Burada <pid> — Java prosesinin identifikatorudur ( jps vasitəsilə öyrənmək olar).
IDE-də adətən sazlama panelində “Thread Dump” düyməsi olur.
Dump-da bunlara baxın:
- Statuslar BLOCKED və ya WAITING.
- waiting to lock ... və locked ... tipli mesajlar.
- JVM ifadəsi: "Found one Java-level deadlock:" — qarşılıqlı bloklama aşkar edildikdə.
Dump-dan nümunə parça:
"Thread-1":
waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
which is held by "Thread-1"
Bu elə deadlock-dur: axınlar bir-birini “tutur”.
VisualVM və digər alətlər
- VisualVM — axınların analizi və deadlock-un aşkarlanması üçün pulsuz alət.
- Java Mission Control / Flight Recorder — inkişaf etmiş monitorinq və profilləmə.
VisualVM-də axın ağacını, onların vəziyyətlərini görə və qarşılıqlı bloklama barədə bildiriş əldə edə bilərsiniz.
Cədvəl: “Deadlock-a necə düşməmək”
| Deadlock-un səbəbi | Necə qarşısını almaq olar |
|---|---|
| Resursların fərqli ardıcıllıqla ələ keçirilməsi | Həmişə resursların ələ keçirilməsinin vahid ardıcıllığına əməl etmək |
| İç-içə synchronized | İç-içəliyi minimuma endirmək, mümkün olduqda tək kiliddən istifadə etmək |
| Kilidin uzun müddət saxlanması | Kritik bölmədə işi azaltmaq |
| Eyni anda bir neçə kiliddən istifadə | tryLock-u taymautla tətbiq etmək və geri almağı bacarmaq |
| Thread-safe olmayan kolleksiyalar | Konkurrent kolleksiyalardan istifadə etmək (ConcurrentHashMap və s.) |
5. Deadlock-la işləyərkən tipik səhvlər
Səhv №1: Resursların fərqli ardıcıllıqla ələ keçirilməsi. Ən yaygın səbəb — müxtəlif axınların kilidləri fərqli ardıcıllıqla götürməsidir. “Belə daha sürətlidir” desəniz belə, vahid ardıcıllığa əməl edin — əks halda dövrəvi gözləmə qaçılmazdır.
Səhv №2: Zərurət olmadan synchronized iç-içə. Artıq iç-içəlik qarşılıqlı bloklama riskini artırır və kodu mürəkkəbləşdirir. Kilidləmə modelini sadələşdirin.
Səhv №3: tryLock və taymautların nəzərə alınmaması. synchronized vasitəsilə kilidləmə axını sonsuzadək gözləməyə məcbur edəcək. Əmin deyilsinizsə, tryLock-dan taymautla və geri alma məntiqi ilə istifadə edin.
Səhv №4: Kilidləmə daxilində kodun uzun icrası. Şəbəkə sorğuları, I/O və ağır hesablamalar kilid altında problem ehtimalını xeyli artırır. Onları kritik bölmədən kənara çıxarın.
Səhv №5: Thread Dump-ı analiz etməmək. Thread Dump — qarşılıqlı bloklamaları tapmaqda ən yaxşı yardımçınızdır. jstack istifadə edin və BLOCKED/WAITING vəziyyətlərini, “holding/waiting to lock” zəncirlərini analiz edin.
GO TO FULL VERSION