CodeGym /Kurslar /JAVA 25 SELF /ReentrantLock və ReadWriteLock: fərqlər, nümunələr

ReentrantLock və ReadWriteLock: fərqlər, nümunələr

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

1. ReentrantLock sinfi: çevik kilidləmə

Açar söz synchronized baza halları üçün çox uyğundur: metodu və ya kod blokunu tez və sadə şəkildə qorumaq olar. Amma bəzən daha çox imkan lazım olur:

  • Kilidi açıq şəkildə idarə etmək (məsələn, onu ələ keçirməyə cəhd etmək, alınmasa — gözləməmək).
  • Resursa “oxuma” və “yazma” hüquqlarını ayırmaq.
  • Kilidi gözləməyi dayandırmaq.
  • Kim və nə zaman kilidi tutdu və ya buraxdı — bunu diaqnoz etmək.

Belə tapşırıqlar üçün ReentrantLock və ReadWriteLock sinifləri yaradılıb. Onlar köhnə, etibarlı synchronized ilə müqayisədə daha çox nəzarət və imkanlar verir.

Bu nədir?

ReentrantLock — Lock interfeysini reallaşdıran sinifdir. Təxminən synchronized kimi işləyir, amma əlavə “fəndlər” ilə. Əsas fərq — kilidə nəzarət açıq olur: siz lock() və unlock() metodlarını özünüz çağırırsınız.

Maraqlı məqam — reentrant o deməkdir ki, axın eyni kilidi ardıcıl bir neçə dəfə tuta bilər və qarşılıqlı bloklama yaratmaz. Bu, metod özünü rekursiv çağıranda və ya çağırışlar zəncirində ümumi kilidlə işləyəndə faydalıdır.

İstifadə sintaksisi

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int value = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // Kilidi ələ keçiririk
        try {
            value++;
        } finally {
            lock.unlock(); // Mütləq kilidi buraxın!
        }
    }

    public int getValue() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}

Diqqət:
lock() və unlock() çağırışları həmişə try...finally konstruksiyası ilə müşayiət olunmalıdır. Əgər unlock() çağırmağı unutsanız, başqa heç bir axın qorunan bloka daxil ola bilməyəcək — “əbədi tıxac” alacaqsınız.

ReentrantLock imkanları

Tutmağa cəhd:
Kilidi tutmağa cəhd etmək olar, lakin sonsuz gözləməmək şərti ilə:

if (lock.tryLock()) {
    try {
        // İşləyirik
    } finally {
        lock.unlock();
    }
} else {
    // Tutmaq alınmadı — başqa iş görürük
}

Vaxt limiti ilə gözləmə:

if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    // 100 ms ərzində ələ keçirildi
}

Kilidin tutulub-tutulmadığını yoxlamaq:

if (lock.isLocked()) { ... }

Gözləmə növbəsinin diaqnostikası, “ədalətliliyi” kilidi və digər imkanlar.

3. Nümunə: ReentrantLock ilə sayğacın artırılması

Gəlin konsol tətbiqimizi inkişaf etdirək (məsələn, müxtəlif axınlardan sifarişlərin emalını imitasiya edək). synchronized və ReentrantLock ilə işin necə göründüyünü müqayisə edək.

synchronized ilə nümunə

public class OrderCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

ReentrantLock ilə eyni nümunə

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OrderCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

Faydası nədədir?

  • Kilidi tutmağa cəhd edə və sonsuz gözləməmək olar (tryLock()).
  • Mürəkkəb məntiq qura bilərsiniz: məsələn, bir neçə kilidi müəyyən ardıcıllıqla tutmaq (mürəkkəb məlumat strukturları üçün aktualdır).
  • Başqa yerdən “kilidi açmaq” mümkündür (amma bunu çox diqqətlə edin — həmişə unlock() barədə xatırlayın!).

4. ReadWriteLock: oxuma və yazma üçün kilid

Bu nədir?

ReadWriteLock sadəcə kilid deyil, ağıllı giriş paylayıcısıdır. Onun əsas reallaşdırması — ReentrantReadWriteLock, kilidləri iki kateqoriyaya bölür: oxuma və yazma üçün.

Axınlar yalnız məlumatı oxuyursa və heç kim heç nə dəyişmirsə, onlar rahatlıqla birlikdə işləyə bilərlər — oxu oxuya mane olmur. Amma kimsə dəyişiklik etmək qərarına gələn kimi, qalanlar gözləməlidir: yazma yalnız bir iştirakçıya icazə verir və sükut tələb edir.

Belə yanaşma oxumaların çox, dəyişikliklərin az olduğu yerlərdə xüsusilə faydalıdır — məsələn, istifadəçilərin daim baxdığı, amma nadir hallarda yenilədiyi məhsul kataloqunda.

İstifadə sintaksisi

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ProductCatalog {
    private final Map<String, String> products = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void addProduct(String id, String name) {
        rwLock.writeLock().lock();
        try {
            products.put(id, name);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public String getProduct(String id) {
        rwLock.readLock().lock();
        try {
            return products.get(id);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

Tətbiqimizdə istifadə nümunəsi

Tutaq ki, bütün axınların oxuduğu sifariş bazamız var (məsələn, sifariş axtarışı üçün), lakin vaxtaşırı yeni sifarişlər gəlir (yazma əməliyyatı).

import java.util.*;
import java.util.concurrent.locks.*;

public class OrderDatabase {
    private final List<String> orders = new ArrayList<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // Sifarişin əlavə edilməsi (writeLock tələb edir)
    public void addOrder(String order) {
        rwLock.writeLock().lock();
        try {
            orders.add(order);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // Bütün sifarişlərin surətini almaq (paralel oxuma mümkündür)
    public List<String> getOrders() {
        rwLock.readLock().lock();
        try {
            // Yarış vəziyyəti olmasın deyə surət qaytarırıq
            return new ArrayList<>(orders);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

Nə baş verir?

  • Heç kim yazmırsa, minlərlə axın eyni vaxtda sifariş siyahısını oxuya bilər.
  • Bir axın sifariş əlavə etməyə başlayan kimi — oxumalar bloklanır ki, “yayılmış” məlumatlar alınmasın.

5. Müqayisə: nə vaxt hansını istifadə etməli?

Ssenari synchronized ReentrantLock ReadWriteLock
Sadə sinxronizasiya ✔ ✔ ✖ (artıqdır)
Taymaut/tutmağa cəhd lazımdır ✖ ✔ ✔
Oxuma əməliyyatları çox, yazma az ✖ ✖ ✔ (əhəmiyyətli artım)
Diaqnostika/statistika tələb olunur ✖ ✔ ✔
Rekursiv kilid ✔ ✔ (reentrancy) ✔

Nəticə:

  • Sadə hallarda — synchronized istifadə edin.
  • Çeviklik üçün — ReentrantLock.
  • “Tez-tez oxuyuruq, nadir hallarda yazırıq” ssenariləri üçün — ReadWriteLock.

6. Vizualizasiya: ReadWriteLock-un iş sxemi

flowchart LR
    subgraph Oxu
      T1[Axın 1] -- Oxu --> Orders
      T2[Axın 2] -- Oxu --> Orders
      T3[Axın 3] -- Oxu --> Orders
    end
    subgraph Yazma
      T4[Axın 4] -- Yazma (addOrder) --> Orders
    end
    Orders[Sifarişlər siyahısı]
    style Orders fill:#f9f,stroke:#333,stroke-width:2px

Heç bir axın yazmırsa, hamı eyni vaxtda oxuya bilər. Yazma əməliyyatı göründüyü anda, qalan axınlar writeLock-un başa çatmasını gözləyir.

7. Reallaşdırma xüsusiyyətləri və nüanslar

“Ədalətlilik” (fairness)

ReentrantLock və ReentrantReadWriteLock üçün “ədalətli” rejimi (fair mode) aktivləşdirmək olar: axınlar növbə qaydasında xidmət olunur, “kim çatdı, o aldı” prinsipi ilə yox. Bu, axınların “ac qalmasını” qarşısını alır, lakin məhsuldarlığı azalda bilər.

Lock fairLock = new ReentrantLock(true); // true — ədalətli rejim
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);

Potensial tələlər

  • Unudulmuş unlock: Əgər unlock() çağırılmasa, əbədi bloklama əldə edəcəksiniz. Həmişə try...finally istifadə edin.
  • Kilid daxilində istisnalar: Kod blokunda istisna baş versə belə, kilid mütləq buraxılmalıdır!
  • ReadWriteLock-un həddindən artıq istifadəsi: Kiçik kolleksiyalar üçün və ya demək olar ki, həmişə yalnız yazma olduqda — ReadWriteLock-un faydası azdır, üstəlik kodu çətinləşdirir.

8. Tipik səhvlər

Səhv №1: unlock() çağırmağı unutdunuz
Ən yayılmış və hiyləgər səhv — kilidi tutduqdan sonra unlock() çağırmağı unutmaqdır. Nəticədə — əbədi bloklama, axınlar “asılı qalır”. Həmişə try...finally istifadə edin, hətta “burada heç nə pozulmayacaq” kimi görünsə belə.

Səhv №2: ReadWriteLock-u lazım olmayan yerdə istifadə etmək
Əgər sizdə paralel oxumalar demək olar ki, yoxdursa, əvəzində yazma tez-tezdirsə, ReadWriteLock yalnız kodu çətinləşdirəcək və məhsuldarlığı azaldacaq. Onu yalnız həqiqətən çoxsaylı eyni vaxtlı oxucular olan yerlərdə istifadə edin.

Səhv №3: bir neçə kilidi fərqli ardıcıllıqla tutmaq
Kodunuz bir neçə Lock tutursa (məsələn, bir neçə obyekt üçün), bunu bütün axınlarda həmişə eyni ardıcıllıqla edin. Əks halda deadlock əldə edə bilərsiniz — axınlar bir-birini sonsuza qədər gözləyəcək.

Səhv №4: synchronized-i ReentrantLock ilə “elə-belə” əvəz etməyə cəhd
Bütün synchronized-ları düşünmədən Lock ilə əvəz etməyə dəyməz — bu, proqramı hər zaman sürətləndirmir və kodu daha az oxunaqlı edə bilər.

Səhv №5: reentrancy-ni unutmaq
Eyni axın lock()-u ardıcıl bir neçə dəfə çağırırsa — bu, ReentrantLock üçün normaldır, amma unutmayın ki, unlock()-u da bir o qədər dəfə çağırmaq lazımdır!

1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Anbarın mərkəzi ehtiyat uçotu 📦
Anbarın mərkəzi ehtiyat uçotu 📦
1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Tətbiqin qlobal konfiqurasiya reyestri ⚙️
Tətbiqin qlobal konfiqurasiya reyestri ⚙️
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION