1. Niyə ReadWriteLock həmişə kifayət etmir
Yazı zamanı yüksək kontensiya problemi
ReadWriteLock (çox vaxt — ReentrantReadWriteLock) əksər axınlar yalnız məlumat oxuduqda, yazanlar isə az olduqda yaxşı işləyir. Bu halda bir neçə axın eyni vaxtda oxuya bilər, yazma isə girişi yalnız qısa müddətlik bloklayır.
Problem o zaman yaranır ki, yazan axınların sayı gözləniləndən çox olur və ya onlar tez-tez oxu ilə yazma arasında keçid edirlər. Əgər yazma əməliyyatları çox vaxt aparırsa, axınlar blokların azad olunmasını daha uzun gözləməyə başlayır. Nəticədə məlumatlara çıxış uğrunda kontensiya artır, oxu və yazma ləngiyir və ReadWriteLock-un effektivliyi düşür.
Bloklamalar arasında bahalı keçid
Axın oxu rejimindən yazma rejiminə (və ya əksinə) keçəndə, daxildə mürəkkəb yoxlama aparılır: kimsənin yazmadığından, bütün oxuyanların çıxmış olduğundan əmin olmaq və yalnız sonra yazmaya icazə vermək lazımdır.
Belə keçidlər tez-tez baş verirsə, bu gecikmələr yaradır. Axınlar növbəyə düzülür və məhsuldarlıq düşür — xüsusən də eyni vaxtda çoxlu oxu və yazma əməliyyatları olduqda.
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
// Oxu
readLock.lock();
try {
// Məlumatı oxuyuruq
} finally {
readLock.unlock();
}
// Yazma
writeLock.lock();
try {
// Məlumatı dəyişirik
} finally {
writeLock.unlock();
}
Hər dəfə axın readLock ilə writeLock arasında keçid edəndə, toqquşmaların qarşısını almaq üçün bütün yoxlamalar aparılır. Belə keçidlər çoxdursa — bu, bahalıdır.
2. StampedLock: sinxronizasiyaya müasir yanaşma
StampedLock — Java 8-də peyda olmuş müasir sinxronizasiya mexanizmidir. O, ReadWriteLock ideyalarını birləşdirir, amma yeni rejim — optimist oxunu əlavə edir və blokların özləri ilə deyil, xüsusi açıq sərbəst buraxılan “ştamplar” (stamps) ilə işləyir.
Əsas xüsusiyyətlər:
- Üç rejim: write lock (eksklüziv yazma), read lock (paylaşılan oxu), optimistic read (bloklamasız optimist oxu).
- Reentrancy yoxdur: eyni axın eyni bloklamaya təkrar daxil ola bilməz.
- Çoxlu oxu və nadir yazı olduqda yüksək məhsuldarlıq.
- Ştampların (stamp) açıq idarəsini tələb edir.
Optimist oxu: tryOptimisticRead + validate
Optimist oxu — axının ümumiyyətlə heç bir bloklama almadan, həmin anda heç kimin yazmadığına “ümid edərək” məlumatı oxuduğu rejimdir. Oxudan sonra axın validate(stamp) metodu ilə oxu zamanı yazmanın olub-olmadığını yoxlamalıdır.
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead();
double currentX = x;
double currentY = y;
// Oxu zamanı yazma olub-olmadığını yoxlayırıq
if (!lock.validate(stamp)) {
// Yazma olubsa — adi read lock götürürük
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.hypot(currentX, currentY);
}
}
Bu necə işləyir:
- tryOptimisticRead() heç kim yazmadığı müddətdə “etibarlı” qalan (long) ştamp qaytarır.
- x və y dəyərlərini oxuyuruq.
- validate(stamp) oxunun əvvəlindən sonuna qədər arada yazma olub-olmadığını yoxlayır.
- Hər şey qaydasındadırsa — oxunan dəyərlərdən istifadə edirik; yazma olubsa — adi readLock götürüb yenidən oxuyuruq.
Bu, yazıların çox nadir, oxuların isə çox olduğu hallarda sərfəlidir. Çox vaxt validate true qaytarır və oxu demək olar ki, pulsuz başa gəlir.
Ssenarilər: “çox oxu — az yazı”
- Məlumat nadir dəyişir, amma tez-tez oxunur (məsələn, keş, koordinatlar, meta-məlumatlar).
- Oxu gecikmələrini minimuma endirmək vacibdir.
- Əgər qəfil yazma olarsa, məlumatı “yenidən oxumaq” mümkündür.
Read lock-a geri dönmə
Əgər optimist oxu uğursuz olarsa (validate false qaytarırsa), axın adi readLock-a “geri düşür”. Bu, düzgünlüyü təmin edir və nadir hallarda baş verir.
3. StampedLock-un gizli tələləri
Reentrancy-nin olmaması
ReentrantReadWriteLock-dan fərqli olaraq, StampedLock təkrar girişi dəstəkləmir. Əgər axın artıq bloklamanı tutubsa və onu yenidən götürməyə çalışsa — deadlock olacaq.
long stamp1 = lock.writeLock();
long stamp2 = lock.writeLock(); // DEADLOCK! Axın özünü gözləyir
Kəsilmələrə həssaslıq
StampedLock axın kəsilmələrinə klassik bloklamalar kimi reaksiya vermir. Əgər axın bloklamanı gözləyərkən kəsilərsə, həmişə dərhal “oyanır” demək deyil. Kəsilməyə sürətli reaksiya vacibdirsə, başqa mexanizmlərdən istifadə edin.
Ştampların düzgün sərbəst buraxılması
Hər bir readLock(), writeLock() və ya tryOptimisticRead() çağırışı unikal (long) ştamp qaytarır. Onu müvafiq unlock metoduna vermək mütləqdir:
- unlockRead(stamp)
- unlockWrite(stamp)
Səhv: Ştampları səhv salmaq və ya unlock çağırmağı unutmaq — bloklamaların “sızmasına” və proqramın donmasına gətirəcək.
4. ReentrantReadWriteLock ilə müqayisə
| Xüsusiyyət | ReentrantReadWriteLock | StampedLock |
|---|---|---|
| Reentrancy (təkrar giriş) | Bəli | Xeyr |
| Optimist oxu | Xeyr | Bəli |
| Yüksək kontensiya zamanı məhsuldarlıq | Orta | Yüksək (yazılar az olduqda) |
| Bloklamanın açıq idarəsi | Xeyr (avtomatik) | Bəli (ştamplar) |
| Kəsilmələrə reaksiya | Bəli | Həmişə yox |
| Ədalətlilik (fairness) | Bəli (aktiv edilə bilər) | Xeyr |
Ədalətlilik rejimləri və starvation-a təsiri
- ReentrantReadWriteLock-da “ədalətli” rejimi (fair) aktivləşdirmək olar ki, axınlar növbə qaydasında xidmət olunsun. Bu, starvation-ın (aclıq) qarşısını alır.
- StampedLock-da ədalətlilik yoxdur: digər axınlar bloklamanı daim “ələ keçirərsə”, bəziləri daha uzun gözləyə bilər. Nadir hallarda starvation mümkündür.
5. Sayğaclar: LongAdder/LongAccumulator vs AtomicLong
Yüksək kontensiya zamanı AtomicLong problemi
AtomicLong — atomik dəyişən olub, axın təhlükəsiz artımı təmin edir. Amma çoxlu axın eyni anda incrementAndGet() çağıranda, hamısı tək bir dəyişən uğrunda “mübarizə aparır”, bu da məhsuldarlığın düşməsinə gətirir.
LongAdder: zolaqlı (striped) sayğaclar
LongAdder problemi başqa cür həll edir: sayğacı bir neçə “zolağa” (stripe) bölür, hər biri öz axın qrupuna xidmət edir. Axın zolaqlardan birini artırır, yekun dəyər isə bütün zolaqların cəmidir.
Üstünlük:
- Yüksək kontensiya zamanı axınlar demək olar ki, bir-birinə mane olmur.
- AtomicLong-dan dəfələrlə daha yüksək məhsuldarlıq.
import java.util.concurrent.atomic.LongAdder;
LongAdder adder = new LongAdder();
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) {
adder.increment();
}
};
Thread[] threads = new Thread[8];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("Yekun dəyər: " + adder.sum());
LongAccumulator
LongAccumulator — LongAdder-in ümumiləşdirilmiş versiyasıdır, burada yığma funksiyasını (məsələn, maksimum, minimum və s.) özünüz təyin edə bilərsiniz.
import java.util.concurrent.atomic.LongAccumulator;
LongAccumulator max = new LongAccumulator(Long::max, Long.MIN_VALUE);
max.accumulate(10);
max.accumulate(42);
max.accumulate(7);
System.out.println("Maksimum: " + max.get()); // 42
Zolaqlı (striped) bloklamalar və kontensiyanın azaldılması
Striped locks texnikası ümumi resursu bir neçə müstəqil hissəyə (zolağa) bölür; hər biri öz bloklaması və ya dəyişəni ilə qorunur. Axınlar zolaqlar üzrə bərabər paylanır, bu da kontensiyanı azaldır və məhsuldarlığı artırır. Məhz bu yanaşmadan LongAdder və LongAccumulator istifadə edir.
6. Təcrübə: oxu üstünlük təşkil edən keş
Vəzifə: bizdə məlumatları olan xəritə (Map) və meta-məlumatlar (məsələn, müraciət sayğacı) var. Oxular tez-tez, yazılar isə nadir hallarda baş verir.
StampedLock ilə reallaşdırma:
import java.util.*;
import java.util.concurrent.locks.StampedLock;
import java.util.concurrent.atomic.LongAdder;
public class MetadataCache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final StampedLock lock = new StampedLock();
private final LongAdder hits = new LongAdder();
public V get(K key) {
long stamp = lock.tryOptimisticRead();
V value = map.get(key);
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
value = map.get(key);
} finally {
lock.unlockRead(stamp);
}
}
if (value != null) hits.increment();
return value;
}
public void put(K key, V value) {
long stamp = lock.writeLock();
try {
map.put(key, value);
} finally {
lock.unlockWrite(stamp);
}
}
public long getHits() {
return hits.sum();
}
}
- Oxu üçün optimist rejimdən istifadə olunur: kimsə yazmırsa — oxu demək olar ki, pulsuzdur.
- Yazı üçün — write lock.
- Müraciətlərin sayılması üçün — LongAdder: yüksək kontensiyada belə artımlar bir-birinə mane olmur.
7. Yük altında LongAdder vs AtomicLong profilinqi
Test: 8 axın, hərəsi 1 mln artım
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class CounterBenchmark {
public static void main(String[] args) throws InterruptedException {
int threads = 8;
int increments = 1_000_000;
// AtomicLong
AtomicLong atomic = new AtomicLong();
long start = System.nanoTime();
Thread[] t1 = new Thread[threads];
for (int i = 0; i < threads; i++) {
t1[i] = new Thread(() -> {
for (int j = 0; j < increments; j++) atomic.incrementAndGet();
});
t1[i].start();
}
for (Thread t : t1) t.join();
long timeAtomic = System.nanoTime() - start;
// LongAdder
LongAdder adder = new LongAdder();
start = System.nanoTime();
Thread[] t2 = new Thread[threads];
for (int i = 0; i < threads; i++) {
t2[i] = new Thread(() -> {
for (int j = 0; j < increments; j++) adder.increment();
});
t2[i].start();
}
for (Thread t : t2) t.join();
long timeAdder = System.nanoTime() - start;
System.out.printf("AtomicLong: %d ms, LongAdder: %d ms%n", timeAtomic / 1_000_000, timeAdder / 1_000_000);
}
}
Tipik nəticə:
AtomicLong: 2500 ms, LongAdder: 200 ms
Nəticə: Yüksək kontensiyada LongAdder AtomicLong-dan dəfələrlə sürətlidir.
8. StampedLock və LongAdder istifadə edərkən tipik səhvlər
Səhv №1: unlockRead/unlockWrite çağırmağı unutmaq. Ştamp azad edilməsə, digər axınlar sonsuz gözləyəcək. Həmişə try/finally istifadə edin!
Səhv №2: reentrancy cəhdi. StampedLock təkrar girişi dəstəkləmir. Eyni axından bloklamanı iki dəfə götürməyin.
Səhv №3: validate-dən yanlış istifadə. tryOptimisticRead-dən sonra validate yoxlanmasa, nekonsistent məlumat ala bilərsiniz.
Səhv №4: yüksək kontensiyada AtomicLong istifadə etmək. AtomicLong 1–2 axın üçün yaxşıdır, amma 8+ axında “dar boğaz” olur. LongAdder istifadə edin.
Səhv №5: zolaqlı bloklamaları unutmaq. Öz striped-lock həllinizi qurursunuzsa, axınların zolaqlar üzrə bərabər paylandığına əmin olun; əks halda bəzi zolaqlar yüklənəcək, digərləri isə boş qalacaq.
Səhv №6: StampedLock-dan ədalətlilik gözləmək. StampedLock axınların xidmət sırasını zəmanət vermir. Nadir hallarda starvation mümkündür.
GO TO FULL VERSION