CodeGym /Kurslar /JAVA 25 SELF /StampedLock və az kontensiyalı sayğaclar

StampedLock və az kontensiyalı sayğaclar

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

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.
  • xy 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

LongAccumulatorLongAdder-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 LongAdderLongAccumulator 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.

1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Uçuş İdarəetmə Mərkəzi: Hündürlüyün Keşləşdirilməsi Sistemi 🛰️
Uçuş İdarəetmə Mərkəzi: Hündürlüyün Keşləşdirilməsi Sistemi 🛰️
1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Dron Naviqatoru: Optimist Mövqe Hesablanması 🚁
Dron Naviqatoru: Optimist Mövqe Hesablanması 🚁
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION