Lock — çoxaxınlı tətbiqlərdə kodun kritik bölmələrinə eksklüziv girişi təmin edən sinxronizasiya mexanizmidir. Oracle, 2024-ə görə, Lock interfeysi ənənəvi synchronized blokları ilə müqayisədə daha çevik sinxronizasiya nəzarəti təqdim edir, o cümlədən timeout ilə ələ keçirmə cəhdləri və bir neçə gözləmə növbəsi dəstəyi.
Əsas məqamlar
Lock — java.util.concurrent.locks paketindən olan, məlumatlara girişi sinxronizasiya etmək üçün açıq ələ keçirmə və buraxma əməliyyatlarını təmin edən interfeysdir. Synchronized-dan fərqli olaraq, Lock proqramçıya bloklama mexanizmi üzərində tam nəzarət verir.
Lock interfeysi Java 5-də daxili synchronized mexanizminə alternativ olaraq ortaya çıxdı. Əsas metodlar — lock, unlock, tryLock və lockInterruptibly. Bloklamalar çoxaxınlı mühitdə məlumatlara təhlükəsiz girişi təşkil etməyə, yarış vəziyyətlərinin və məlumatların zədələnməsinin qarşısını almağa imkan verir.
Lock-un synchronized-dan əsas üstünlüyü — çeviklik. Proqramçı timeout ilə bloklamanı ələ keçirməyə cəhd edə, bloklamadan məhrumiyyətlə onun məşğulluğunu yoxlaya və ya müxtəlif prioritetlərlə bir neçə gözləmə növbəsi təşkil edə bilər.
Java 5-də Lock interfeysi yaranana qədər sinxronizasiyanın yeganə yolu synchronized idi, o da məhdudiyyətlərdən əziyyət çəkirdi: timeoutların olmaması, gözləməni kəsmək mümkünsüzlüyü və tək növbə. Doug Lea java.util.concurrent paketini layihələndirərək Lock-u əsas tikinti bloku kimi daxil etdi.
Bloklama daxili vəziyyət bayrağı və gözləmə növbəsi vasitəsilə girişi idarə edir. Axın lock() çağırdıqda, mexanizm bloklamanın sərbəst olub-olmadığını yoxlayır və ya onu ələ keçirir, ya da axını buraxılana qədər növbəyə yerləşdirir.
Hər hansı bloklamanın əsasında atomik müqayisə və yerləşdirmə (CAS) əməliyyatı dayanır. Lock() çağırışı zamanı axın məşğulluq bayrağını atomik olaraq yerləşdirməyə cəhd edir. Bayraq artıq yerləşdirilibsə, axın bloklanır. Unlock() zamanı bayraq sıfırlanır və gözləyən axınlardan biri oyadılır.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// kritik bölmə
println("${Thread.currentThread().name} axını işləyir")
} finally {
lock.unlock()
}
}
ReentrantLock daxildə ikitərəfli növbədən (CLH lock queue) istifadə edir, burada hər gözləyən axın düyünlə təmsil olunur. Bloklama buraxıldıqda, növbənin baş düyünü oyadılır. Fair (ədalətli) rejim FIFO sırasını təmin edir, unfair isə ötürmə qabiliyyətini artırmaq üçün yeni axının gözləyənlərdən əvvəl ələ keçirməsinə icazə verir.
Müasir Java-Stack-də bloklamaların bir neçə tətbiqi mövcuddur, hər biri konkret ssenarilər üçün optimallaşdırılmışdır. Düzgün bloklamanın seçilməsi birbaşa çoxaxınlı tətbiqin məhsuldarlığına və etibarlılığına təsir edir.
ReentrantLock — Lock-un əsas və ən çox istifadə edilən tətbiqidir. Eyni axın tərəfindən təkrar ələ keçirməyi dəstəkləyir: əgər axın artıq bloklamaya sahibdirsə, təkrar lock() çağırışı onu bloklamır. Bu, rekursiv çağırışlarda deadlock-un qarşısını alır.
ReadWriteLock bloklamaları iki rejimə ayırır: oxu və yazı. Bir neçə axın eyni anda oxu bloklamasını saxlaya bilər, lakin yazı eksklüziv giriş tələb edir. Bu, tez-tez oxuma və nadir yazma zamanı məhsuldarlığı əhəmiyyətli dərəcədə artırır.
StampedLock — Java 8-də ortaya çıxan ən yeni tətbiqdir. Üç rejimi dəstəkləyir: yazı, oxu və optimist oxu. Optimist oxu digər axınları bloklamır və oxuduqdan sonra məlumatların etibarlılığını yoxlayır ki, bu da ReadWriteLock ilə müqayisədə 10-20% məhsuldarlıq artımı verir.
| Bloklama | Java versiyası | Rejimlər | Məhsuldarlıq |
|---|---|---|---|
| ReentrantLock | Java 5 | eksklüziv | yüksək |
| ReadWriteLock | Java 5 | oxu + yazı | orta |
| StampedLock | Java 8 | oxu + yazı + optimistic | çox yüksək |
ReentrantLock — Lock-un ən populyar tətbiqi olub, synchronized-da mövcud olmayan bir sıra imkanlar təqdim edir. Onun xüsusiyyətlərini başa düşmək çoxaxınlılıqla effektiv iş üçün zəruridir.
ReentrantLock konstruktoru fair parametrini qəbul edir. True olduqda bloklama FIFO sırasını təmin edir, false olduqda yeni axının gözləyənlərdən əvvəl ələ keçirməsi mümkündür. Ədalətli rejim aclığın qarşısını alır, lakin növbənin saxlanması üçün əlavə xərclər səbəbindən ötürmə qabiliyyətini 10-20% azaldır.
Synchronized-dan fərqli olaraq, ReentrantLock tryLock-u timeout ilə dəstəkləyir. Bloklamanı göstərilən müddətdə ələ keçirmək mümkün olmadıqda, axın sonsuz bloklanmaq əvəzinə icranı davam etdirir. LockInterruptibly metodu Thread.interrupt() vasitəsilə gözləyən axını kəsməyə imkan verir.
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Bloklama ələ keçirildi")
} finally {
lock.unlock()
}
} else {
println("Bloklamanı ələ keçirmək mümkün olmadı")
}
}
ReentrantLock newCondition() metodu vasitəsilə bir neçə şərt dəyişənini dəstəkləyir. Hər Condition-un öz gözləmə növbəsi var ki, bu da mürəkkəb oyadılma ssenarilərini təşkil etməyə imkan verir. Await() və signal() metodları synchronized bloklarından wait() və notify()-u əvəz etdi, lakin bir neçə növbə dəstəyi ilə.
ReadWriteLock və StampedLock oxu əməliyyatlarının yazıdan üstün olduğu hallarda girişin optimallaşdırılması problemini həll edir. Onlar oxumanın yazmadan daha tez-tez baş verdiyi ssenarilərdə ReentrantLock-dan qat-qat səmərəlidir.
ReadWriteLock interfeysi iki metoddan ibarətdir: readLock() və writeLock(). Oxu bloklaması eyni anda bir neçə axın tərəfindən saxlanıla bilər, yazı bloklaması isə yalnız bir axın tərəfindən. Tipik nümunə — axın təhlükəsiz keş: çoxlu axın məlumatları oxuyur və yalnız bir axın vaxtaşırı onları yeniləyir.
class SafeCache<K, V> {
private val map = mutableMapOf<K, V>()
private val rwLock = ReentrantReadWriteLock()
fun get(key: K): V? {
rwLock.readLock().lock()
return try { map[key] } finally { rwLock.readLock().unlock() }
}
fun put(key: K, value: V) {
rwLock.writeLock().lock()
return try { map[key] = value } finally { rwLock.writeLock().unlock() }
}
}
StampedLock üçüncü rejimi əlavə edir — tryOptimisticRead. Bu rejim digər axınları bloklamır, yalnız vəziyyətin ştampını (stamp) yadda saxlayır. Oxuduqdan sonra proqramçı validate(stamp) çağıraraq məlumatların oxuma zamanı dəyişib-dəyişmədiyini yoxlayır. Məlumatlar dəyişibsə, əməliyyat təkrarlanmalıdır.
Mobil tətbiqlərdə bloklamalar axınlar arasında ümumi məlumatlara girişi koordinasiya etmək üçün istifadə olunur. Lakin onların tətbiqi məhdud cihaz resursları və interfeysin cavabdehliyini qorumaq zərurəti səbəbindən xüsusi diqqət tələb edir.
Android-də ReentrantLock Room, keşlər və fayllarla işləyərkən faydalıdır. Əsas axında bloklamanı ələ keçirməmək vacibdir. Asinxron kod üçün axını bloklamayan, korutini dayandıran kotlinx.coroutines-dən korutinlər və Mutex daha üstündür.
iOS-da NSLock-dan standart Lock daha az istifadə olunur — proqramçılar DispatchQueue-ya barrier bayraqları ilə və ya os_unfair_lock əməliyyat bloklamalarına üstünlük verirlər. Swift 5.7+ vəziyyəti avtomatik qoruyan actors vasitəsilə müasir sinxronizasiya mexanizmləri təmin edir.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
Deadlock-dan qaçmaq üçün layihədə bütün bloklamaların vahid ələ keçirmə sırasına riayət edin. Uzun müddətli bloklamanın mümkün olduğu yerlərdə lock() əvəzinə timeout ilə tryLock istifadə edin. Ənənəvi bloklamalar əvəzinə Lock-Free alqoritmlərini (AtomicReference, ConcurrentHashMap) tətbiq etməyi düşünün.
Lock-un tətbiqi deadlock və məhsuldarlığın düşməsinin qarşısını alan bir neçə qaydaya riayət etməyi tələb edir. Bu təcrübələr Java icması tərəfindən java.util.concurrent paketinin 20 illik istifadəsi ərzində işlənib hazırlanmışdır.
Ən vacib nümunə — finally-də lock. Kritik bölmənin uğurla və ya istisna ilə bitməsindən asılı olmayaraq, bloklama buraxılmalıdır. Bu, digər axınların bir səhv səbəbindən əbədi bloklanmayacağına zəmanət verir. Kotlin-də bu nümunə withLock genişlənməsi vasitəsilə zərif şəkildə həll olunur.
Kritik bölmə maksimum qısa olmalıdır. Bloklama daxilində heç vaxt giriş-çıxış, şəbəkə sorğuları və ya uzunmüddətli hesablamalar yerinə yetirməyin. Əgər serverdən məlumat oxumaq lazımdırsa, əvvəlcə onları əldə edin, sonra yalnız paylaşılan vəziyyəti yeniləmək üçün bloklamanı ələ keçirin. Bu, rəqabəti azaldır və sistemin ötürmə qabiliyyətini artırır.
Bir neçə Lock ilə işləyərkən deadlock-un qarşısını almaq üçün bütün layihədə qlobal ələ keçirmə sırasını müəyyən edin. Əgər əvvəlcə lockA, sonra lockB ələ keçirilirsə — hər hansı tərs ardıcıllıq code review qaydaları ilə qadağan edilməlidir. Avtomatik yoxlama üçün SpotBugs və IntelliJ Inspections kimi statik analizatorlardan istifadə edin.
Tez-tez verilən suallar
Lock — timeout və kəsilə bilən gözləmə imkanı olan açıq interfeysdir. synchronized avtomatik olaraq monitoru ələ keçirir və buraxır, lakin tryLock, lockInterruptibly və bir neçə Condition-dan istifadə etməyə imkan vermir. Lock daha çevikdir, lakin finally-də əl ilə buraxılma tələb edir.
Ədalətli bloklama FIFO sırasını təmin edir: ən uzun gözləyən axın bloklamanı birinci alır. Ədalətsiz bloklama növbədən yan keçərək yeni axına giriş verə bilər ki, bu da ötürmə qabiliyyətini artırır, lakin gözləyən axınların aclığına səbəb ola bilər.
Bütün bloklamaların sabit sırasına riayət edin, şərtsiz lock əvəzinə timeout ilə tryLock istifadə edin və eyni anda saxlanılan bloklamaların sayını minimuma endirin. Lock-Free məlumat strukturlarının tətbiqi də deadlock riskini azaldır.
Condition — Lock üçün wait/notify analoqudur, bir neçə müstəqil gözləmə növbəsi təşkil etməyə imkan verir. Hər newCondition() çağırışı ayrıca növbə yaradır ki, bu da synchronized-in tək növbəsi ilə müqayisədə axınların oyadılması üzərində daha dəqiq nəzarət verir.
Korutinləri olan Android üçün kotlinx.coroutines-dən Mutex istifadə edin — o, axını bloklamaq əvəzinə korutini dayandırır. Swift 5.7+ ilə iOS üçün vəziyyətə girişi avtomatik sinxronizasiya edən actors daha üstündür. ReentrantLock-u köhnə kod və aşağı səviyyəli ssenarilər üçün saxlayın.
Nəticə
Açar təslim mobil tətbiq hazırlayacağıq
IT Sectr 2017-ci ildən startaplar və bizneslər üçün iOS və Android tətbiqləri yaradır. Sizə məsləhət verəcəyik və ən yaxşı həlli təklif edəcəyik.
Həm də oxuyun