Lock is een synchronisatiemechanisme dat exclusieve toegang biedt tot kritieke secties van code in multithreading-applicaties. Volgens Oracle, 2024 biedt de Lock-interface een flexibelere synchronisatiecontrole vergeleken met traditionele synchronized-blokken, inclusief pogingen tot vergrendeling met timeout en ondersteuning voor meerdere wachtrijen.
Belangrijkste punten
Lock is een interface uit het pakket java.util.concurrent.locks die expliciete vergrendelings- en ontgrendelingsoperaties biedt voor synchronisatie van gegevens toegang. In tegenstelling tot synchronized geeft Lock de ontwikkelaar volledige controle over het vergrendelingsmechanisme.
De Lock-interface verscheen in Java 5 als alternatief voor het ingebouwde synchronized-mechanisme. De belangrijkste methoden zijn lock, unlock, tryLock en lockInterruptibly. Vergrendelingen maken veilige toegang tot gegevens in een multithreading-omgeving mogelijk, waardoor racecondities en gegevensbeschadiging worden voorkomen.
Het belangrijkste voordeel van Lock ten opzichte van synchronized is flexibiliteit. De ontwikkelaar kan proberen een vergrendeling met timeout te verkrijgen, de bezetting controleren zonder te blokkeren, of meerdere wachtrijen met verschillende prioriteiten organiseren.
Vóór de komst van de Lock-interface in Java 5 was de enige manier van synchronisatie synchronized, dat leed onder beperkingen: gebrek aan timeouts, onmogelijkheid om wachten te onderbreken en een enkele wachtrij. Doug Lea ontwierp het pakket java.util.concurrent en nam Lock op als fundamenteel bouwblok.
Vergrendeling beheert toegang via een interne statusvlag en een wachtrij. Wanneer een thread lock() aanroept, controleert het mechanisme of de vergrendeling vrij is en vergrendelt deze of plaatst de thread in de wachtrij tot vrijgave.
Aan de basis van elke vergrendeling ligt een atomaire vergelijk-en-vervang (CAS)-operatie. Bij lock() probeert de thread atomair de bezetvlag te zetten. Als de vlag al is gezet, wordt de thread geblokkeerd. Bij unlock() wordt de vlag gereset en wordt een van de wachtende threads gewekt.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// kritieke sectie
println("Thread ${Thread.currentThread().name} werkt")
} finally {
lock.unlock()
}
}
ReentrantLock gebruikt intern een tweerichtingswachtrij (CLH lock queue), waarbij elke wachtende thread wordt vertegenwoordigd door een knoop. Wanneer de vergrendeling wordt vrijgegeven, wordt de hoofdknoop van de wachtrij gewekt. De fair-modus garandeert FIFO-volgorde, terwijl unfair het mogelijk maakt dat een nieuwe thread vóór wachtenden vergrendelt voor hogere doorvoer.
In de moderne Java-stack bestaan meerdere implementaties van vergrendelingen, elk geoptimaliseerd voor specifieke scenario's. De juiste keuze van vergrendeling beïnvloedt direct de prestaties en betrouwbaarheid van een multithreading-applicatie.
ReentrantLock — de basis en meest gebruikte implementatie van Lock. Het ondersteunt hervergrendeling door dezelfde thread: als een thread de vergrendeling al bezit, blokkeert een herhaalde lock()-aanroep niet. Dit voorkomt deadlock bij recursieve aanroepen.
ReadWriteLock splitst vergrendelingen in twee modi: lezen en schrijven. Meerdere threads kunnen gelijktijdig de leesvergrendeling vasthouden, maar schrijven vereist exclusieve toegang. Dit verhoogt de prestaties aanzienlijk bij veel lezen en weinig schrijven.
StampedLock — de nieuwste implementatie, verschenen in Java 8. Het ondersteunt drie modi: schrijven, lezen en optimistisch lezen. Optimistisch lezen blokkeert andere threads niet en controleert de geldigheid van gegevens na het lezen, wat een prestatieverbetering van 10-20% oplevert ten opzichte van ReadWriteLock.
| Vergrendeling | Java-versie | Modi | Prestaties |
|---|---|---|---|
| ReentrantLock | Java 5 | exclusief | hoog |
| ReadWriteLock | Java 5 | lezen + schrijven | gemiddeld |
| StampedLock | Java 8 | lezen + schrijven + optimistic | zeer hoog |
ReentrantLock — de populairste implementatie van Lock, die een reeks mogelijkheden biedt die niet beschikbaar zijn in synchronized. Inzicht in de kenmerken is essentieel voor effectief werken met multithreading.
De constructor van ReentrantLock accepteert de parameter fair. Bij true garandeert de vergrendeling FIFO-volgorde van toegang, bij false is vergrendeling door een nieuwe thread vóór wachtenden mogelijk. Eerlijke modus voorkomt uithongering, maar verlaagt de doorvoer met 10-20% vanwege extra overhead voor het onderhouden van de wachtrij.
In tegenstelling tot synchronized ondersteunt ReentrantLock tryLock met timeout. Als de vergrendeling niet binnen de opgegeven tijd kan worden verkregen, gaat de thread verder met uitvoeren in plaats van oneindig te blokkeren. De methode lockInterruptibly maakt het mogelijk een wachtende thread te onderbreken via Thread.interrupt().
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Vergrendeling verkregen")
} finally {
lock.unlock()
}
} else {
println("Vergrendeling kon niet worden verkregen")
}
}
ReentrantLock ondersteunt meerdere conditievariabelen via de methode newCondition(). Elke Condition heeft een eigen wachtrij, wat het mogelijk maakt complexe wekscenario's te organiseren. De methoden await() en signal() hebben wait() en notify() uit synchronized-blokken vervangen, maar met ondersteuning voor meerdere wachtrijen.
ReadWriteLock en StampedLock lossen het probleem van toegangsoptimalisatie op wanneer leesoperaties overheersen over schrijven. Ze zijn aanzienlijk efficiënter dan ReentrantLock in scenario's waar lezen vaker voorkomt dan schrijven.
De ReadWriteLock-interface bevat twee methoden: readLock() en writeLock(). De leesvergrendeling kan gelijktijdig door meerdere threads worden vastgehouden, de schrijfvergrendeling slechts door één. Een typisch voorbeeld — een thread-safe cache: meerdere threads lezen gegevens en slechts één werkt ze periodiek bij.
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 voegt een derde modus toe — tryOptimisticRead. Deze modus blokkeert andere threads niet, maar onthoudt alleen een stempel (stamp) van de status. Na het lezen roept de ontwikkelaar validate(stamp) aan om te controleren of de gegevens zijn gewijzigd tijdens het lezen. Als de gegevens zijn gewijzigd, moet de operatie worden herhaald.
In mobiele applicaties worden vergrendelingen gebruikt voor coördinatie van toegang tot gedeelde gegevens tussen threads. Het gebruik ervan vereist echter speciale voorzichtigheid vanwege de beperkte apparaatbronnen en de noodzaak om de responsiviteit van de interface te behouden.
Op Android is ReentrantLock nuttig bij het werken met Room, caches en bestanden. Het is belangrijk te onthouden: vergrendel nooit op de hoofdthread. Voor asynchrone code verdienen coroutines en Mutex uit kotlinx.coroutines de voorkeur, die de thread niet blokkeren maar de coroutine onderbreken.
In iOS wordt standaard Lock van NSLock minder gebruikt — ontwikkelaars geven de voorkeur aan DispatchQueue met barrier-vlaggen of operationele vergrendelingen os_unfair_lock. Swift 5.7+ biedt moderne synchronisatiemechanismen via actors, die automatisch de status beschermen.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
Om deadlock te voorkomen, volg één enkele vergrendelingsvolgorde voor alle vergrendelingen in het project. Gebruik tryLock met timeout in plaats van lock() overal waar langdurige blokkering mogelijk is. Overweeg Lock-Free algoritmen (AtomicReference, ConcurrentHashMap) in plaats van traditionele vergrendelingen.
Het gebruik van Lock vereist discipline en naleving van verschillende regels die deadlock en prestatieverlies voorkomen. Deze praktijken zijn ontwikkeld door de Java-gemeenschap in 20 jaar gebruik van het pakket java.util.concurrent.
Het belangrijkste patroon — lock in finally. Ongeacht of de kritieke sectie succesvol of met een uitzondering is beëindigd, moet de vergrendeling worden vrijgegeven. Dit garandeert dat andere threads niet voor altijd worden geblokkeerd door één fout. In Kotlin wordt dit patroon elegant opgelost via de extensie withLock.
De kritieke sectie moet maximaal kort zijn. Voer nooit invoer-uitvoer, netwerkverzoeken of lange berekeningen uit binnen een vergrendeling. Als u gegevens van een server moet lezen, haal ze dan eerst op en vergrendel alleen voor het bijwerken van de gedeelde status. Dit vermindert concurrentie en verhoogt de systeemdoorvoer.
Om deadlock bij het werken met meerdere Lock te voorkomen, stel globale vergrendelingsvolgorde vast in het hele project. Als eerst lockA en daarna lockB wordt verkregen — moet elke omgekeerde volgorde worden verboden door code review-regels. Gebruik statische analysatoren zoals SpotBugs en IntelliJ Inspections voor automatische controle.
Veelgestelde vragen
Lock — expliciete interface met mogelijkheid tot timeout en onderbreekbaar wachten. synchronized vergrendelt en ontgrendelt automatisch de monitor, maar staat geen tryLock, lockInterruptibly en meerdere Condition toe. Lock is flexibeler, maar vereist handmatige vrijgave in finally.
Een eerlijke vergrendeling garandeert FIFO-volgorde van toegang: de thread die het langst wacht, krijgt als eerste de vergrendeling. Oneerlijke vergrendeling kan toegang geven aan een nieuwe thread die de wachtrij omzeilt, wat de doorvoer verhoogt maar kan leiden tot uithongering van wachtende threads.
Volg vaste volgorde van vergrendeling van alle locks, gebruik tryLock met timeout in plaats van onvoorwaardelijke lock en minimaliseer het aantal gelijktijdig vastgehouden vergrendelingen. Toepassing van Lock-Free gegevensstructuren vermindert ook het risico op deadlock.
Condition — de tegenhanger van wait/notify voor Lock, waarmee meerdere onafhankelijke wachtrijen kunnen worden georganiseerd. Elke aanroep van newCondition() creëert een aparte wachtrij, wat meer precieze controle geeft over het wekken van threads vergeleken met de enkele wachtrij van synchronized.
Voor Android met coroutines gebruik Mutex van kotlinx.coroutines — het onderbreekt de coroutine, niet de thread. Voor iOS met Swift 5.7+ hebben actors de voorkeur, die automatisch toegang tot de status synchroniseren. Houd ReentrantLock voor legacy-code en low-level scenario's.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook