Lock, çok iş parçacıklı uygulamalarda kodun kritik bölümlerine özel erişim sağlayan bir senkronizasyon mekanizmasıdır. Oracle, 2024'e göre, Lock arayüzü geleneksel synchronized bloklarına kıyasla daha esnek senkronizasyon kontrolü sağlar; zaman aşımı ile edinme denemeleri ve birden çok bekleme kuyruğu desteğini içerir.
Önemli Noktalar
Lock, veri erişimini senkronize etmek için açık kilitleme ve kilit açma işlemleri sağlayan java.util.concurrent.locks paketindeki bir arayüzdür. synchronized'dan farklı olarak Lock, geliştiriciye kilitleme mekanizması üzerinde tam kontrol sağlar.
Lock arayüzü, Java 5'te yerleşik synchronized mekanizmasına bir alternatif olarak tanıtıldı. Ana yöntemler lock, unlock, tryLock ve lockInterruptibly'dir. Kilitler, çok iş parçacıklı bir ortamda güvenli veri erişimi düzenlemeye yardımcı olur ve yarış koşullarını ve veri bozulmasını önler.
Lock'un synchronized'a göre ana avantajı esnekliktir. Geliştirici, zaman aşımı ile bir kilidi edinmeyi deneyebilir, bloke olmadan kullanılabilirliğini kontrol edebilir veya farklı önceliklerle birden çok bekleme kuyruğu düzenleyebilir.
Lock arayüzü Java 5'te ortaya çıkmadan önce, tek senkronizasyon yöntemi synchronized idi ve zaman aşımı olmaması, kesilebilir bekleme olmaması ve tek kuyruk gibi sınırlamalardan muzdaripti. Doug Lea, java.util.concurrent paketini tasarladı ve Lock'u temel bir yapı taşı olarak dahil etti.
Bir kilit, dahili bir durum bayrağı ve bekleme kuyruğu aracılığıyla erişimi yönetir. Bir iş parçacığı lock() çağırdığında, mekanizma kilidin boş olup olmadığını kontrol eder ve ya onu edinir ya da serbest bırakılana kadar iş parçacığını kuyruğa alır.
Herhangi bir kilidin merkezinde bir atomik karşılaştır-ve-değiştir (CAS) işlemi bulunur. lock() çağrıldığında, iş parçacığı atomik olarak meşgul bayrağını ayarlamaya çalışır. Bayrak zaten ayarlanmışsa, iş parçacığı bloke olur. unlock() ile bayrak temizlenir ve bekleyen bir iş parçacığı uyandırılır.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// kritik bölüm
println("${Thread.currentThread().name} iş parçacığı çalışıyor")
} finally {
lock.unlock()
}
}
ReentrantLock dahili olarak her bekleyen iş parçacığının bir düğümle temsil edildiği çift yönlü bağlı bir liste (CLH kilit kuyruğu) kullanır. Kilit serbest bırakıldığında, kuyruğun baş düğümü uyandırılır. Adil mod (fair) FIFO sırasını garanti ederken, adil olmayan mod verimi artırmak için yeni bir iş parçacığının bekleyenlerden önce kilidi edinmesine izin verir.
Modern Java ekosisteminde, her biri belirli senaryolar için optimize edilmiş birden çok kilit uygulaması bulunur. Doğru kilidi seçmek, çok iş parçacıklı bir uygulamanın performansını ve güvenilirliğini doğrudan etkiler.
ReentrantLock, Lock'un temel ve en çok kullanılan uygulamasıdır. Aynı iş parçacığı tarafından yeniden edinmeyi destekler: bir iş parçacığı kilidi zaten tutuyorsa, lock()'u tekrar çağırmak onu bloke etmez. Bu, özyinelemeli çağrılarda deadlock'u önler.
ReadWriteLock, kilitleri iki moda ayırır: okuma ve yazma. Birden çok iş parçacığı aynı anda okuma kilidini tutabilir, ancak yazma özel erişim gerektirir. Bu, sık okuma ve seyrek yazma durumlarında performansı önemli ölçüde artırır.
StampedLock, Java 8'de tanıtılan en yeni uygulamadır. Üç modu destekler: yazma, okuma ve iyimser okuma. İyimser okuma diğer iş parçacıklarını bloke etmez ve okumadan sonra verileri doğrular, ReadWriteLock'a göre %10-20 performans avantajı sağlar.
| Kilit | Java Sürümü | Modlar | Performans |
|---|---|---|---|
| ReentrantLock | Java 5 | özel | yüksek |
| ReadWriteLock | Java 5 | okuma + yazma | orta |
| StampedLock | Java 8 | okuma + yazma + iyimser | çok yüksek |
ReentrantLock, synchronized'da bulunmayan birçok özellik sunan en popüler Lock uygulamasıdır. Özelliklerini anlamak, etkili çoklu iş parçacığı çalışması için gereklidir.
ReentrantLock yapıcısı bir fair parametresi alır. true olduğunda, kilit FIFO sırasını garanti eder; false olduğunda, yeni bir iş parçacığı bekleyenlerden önce kilidi edinebilir. Adil mod açlığı önler ancak kuyruk bakımının ek yükü nedeniyle verimi %10-20 azaltır.
synchronized'dan farklı olarak, ReentrantLock zaman aşımı ile tryLock'u destekler. Kilit belirtilen süre içinde edinilemezse, iş parçacığı süresiz olarak bloke olmak yerine yürütmeye devam eder. lockInterruptibly yöntemi, Thread.interrupt() aracılığıyla bekleyen bir iş parçacığını kesmeye izin verir.
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Kilit edinildi")
} finally {
lock.unlock()
}
} else {
println("Kilit edinilemedi")
}
}
ReentrantLock, newCondition() yöntemi aracılığıyla birden çok koşul değişkenini destekler. Her Condition'ın kendi bekleme kuyruğu vardır ve karmaşık uyandırma senaryolarına olanak tanır. await() ve signal() yöntemleri, synchronized bloklarındaki wait() ve notify()'un yerini almıştır, ancak birden çok kuyruk desteğiyle.
ReadWriteLock ve StampedLock, okumaların yazmalara göre daha baskın olduğu durumlarda erişim optimizasyonunu ele alır. Okumanın yazmadan daha sık gerçekleştiği senaryolarda ReentrantLock'tan önemli ölçüde daha verimlidirler.
ReadWriteLock arayüzü iki yöntem içerir: readLock() ve writeLock(). Okuma kilidi birden çok iş parçacığı tarafından aynı anda tutulabilirken, yazma kilidi özeldir. Tipik bir örnek, iş parçacığı güvenli bir önbellektir: birçok iş parçacığı verileri okurken yalnızca biri periyodik olarak günceller.
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ü bir mod ekler — tryOptimisticRead. Bu mod diğer iş parçacıklarını bloke etmez, yalnızca bir durum damgası (stamp) kaydeder. Okumadan sonra, geliştirici okuma sırasında verilerin değişip değişmediğini kontrol etmek için validate(stamp) çağırır. Veriler değişmişse, işlem tekrarlanmalıdır.
Mobil uygulamalarda, kilitler iş parçacıkları arasında paylaşılan verilere erişimi koordine etmek için kullanılır. Ancak, sınırlı cihaz kaynakları ve UI yanıt verebilirliğini sürdürme ihtiyacı nedeniyle kullanımları özel dikkat gerektirir.
Android'de ReentrantLock, Room, önbellekler ve dosyalarla çalışırken kullanışlıdır. Hatırlanması önemli: ana iş parçacığında asla kilit edinmeyin. Zaman uyumsuz kod için, kotlinx.coroutines'den coroutine'ler ve Mutex tercih edilir; bunlar iş parçacığını bloke etmek yerine coroutine'i askıya alır.
iOS'te standart NSLock daha az kullanılır — geliştiriciler bariyer bayraklarıyla DispatchQueue veya os_unfair_lock'u tercih eder. Swift 5.7+, durumu otomatik olarak koruyan aktörler aracılığıyla modern senkronizasyon mekanizmaları sağlar.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
Deadlock'ları önlemek için proje genelinde tutarlı bir kilit sırası izleyin. Uzun süreli bloke olmanın mümkün olduğu her yerde lock() yerine zaman aşımı ile tryLock kullanın. Geleneksel kilitler yerine Lock-Free algoritmaları (AtomicReference, ConcurrentHashMap) kullanmayı düşünün.
Lock kullanımı, deadlock'ları ve performans düşüşünü önleyen disiplin ve birkaç kurala uymayı gerektirir. Bu uygulamalar, Java topluluğu tarafından java.util.concurrent paketinin 20 yıllık kullanımı boyunca geliştirilmiştir.
En önemli desen finally'de kilidi serbest bırakmaktır. Kritik bölüm başarıyla tamamlansın veya bir istisna atsın, kilit serbest bırakılmalıdır. Bu, diğer iş parçacıklarının tek bir hata nedeniyle sonsuza kadar bloke olmamasını sağlar. Kotlin'de bu desen, withLock uzantısı ile zarif bir şekilde çözülür.
Kritik bölüm mümkün olduğunca kısa olmalıdır. Bir kilit içinde asla G/Ç, ağ istekleri veya uzun hesaplamalar yapmayın. Bir sunucudan veri okumanız gerekiyorsa, önce verileri alın, ardından yalnızca paylaşılan durumu güncellemek için kilidi edinin. Bu, rekabeti azaltır ve sistem verimini artırır.
Birden çok kilit ile çalışırken deadlock'ları önlemek için proje genelinde küresel bir kilit sırası oluşturun. Önce lockA, ardından lockB ediniliyorsa, herhangi bir ters sıra kod inceleme kuralları tarafından yasaklanmalıdır. Otomatik doğrulama için SpotBugs ve IntelliJ Inspections gibi statik analizörler kullanın.
Sıkça Sorulan Sorular
Lock, zaman aşımı ve kesilebilir bekleme desteği olan açık bir arayüzdür. synchronized otomatik olarak monitörü edinir ve serbest bırakır, ancak tryLock, lockInterruptibly veya birden çok Condition kullanımına izin vermez. Lock daha esnektir ancak finally'de manuel serbest bırakma gerektirir.
Adil kilit FIFO sırasını garanti eder: en uzun süredir bekleyen iş parçacığı kilidi ilk alır. Adil olmayan kilit, bekleyenlerden önce yeni bir iş parçacığına erişim verebilir, bu da verimi artırır ancak bekleyen iş parçacıklarının aç kalmasına neden olabilir.
Tüm kilitleri edinmek için sabit bir sıra izleyin, koşulsuz lock() yerine zaman aşımı ile tryLock kullanın ve aynı anda tutulan kilit sayısını en aza indirin. Lock-Free veri yapılarının kullanımı da deadlock riskini azaltır.
Condition, Lock için wait/notify benzeridir ve birden çok bağımsız bekleme kuyruğuna izin verir. Her newCondition() çağrısı ayrı bir kuyruk oluşturur ve synchronized'ın tek kuyruğuna kıyasla iş parçacığı uyandırma üzerinde daha hassas kontrol sağlar.
Coroutine'ler ile Android için kotlinx.coroutines'den Mutex kullanın — iş parçacığını bloke etmek yerine coroutine'i askıya alır. Swift 5.7+ ile iOS için, durum erişimini otomatik olarak senkronize eden aktörler tercih edilir. ReentrantLock'u eski kod ve düşük seviyeli senaryolar için saklayın.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun