Lock är en synkroniseringsmekanism som ger exklusiv åtkomst till kritiska sektioner av kod i flertrådade applikationer. Enligt Oracle, 2024 ger Lock-gränssnittet mer flexibel synkroniseringskontroll jämfört med traditionella synchronized-block, inklusive försök till låsning med timeout och stöd för flera vänteköer.
Huvudpunkter
Lock är ett gränssnitt från paketet java.util.concurrent.locks som tillhandahåller explicita låsnings- och upplåsningsoperationer för synkronisering av dataåtkomst. Till skillnad från synchronized ger Lock utvecklaren full kontroll över låsningsmekanismen.
Lock-gränssnittet dök upp i Java 5 som ett alternativ till den inbyggda synchronized-mekanismen. Huvudmetoderna är lock, unlock, tryLock och lockInterruptibly. Låsningar möjliggör organisering av säker dataåtkomst i en flertrådad miljö, vilket förhindrar race conditions och dataskada.
Den främsta fördelen med Lock jämfört med synchronized är flexibilitet. Utvecklaren kan försöka låsa med timeout, kontrollera upptagenhet utan blockering, eller organisera flera vänteköer med olika prioriteter.
Innan Lock-gränssnittet kom i Java 5 var det enda sättet för synkronisering synchronized, som led av begränsningar: brist på timeout, omöjlighet att avbryta väntan och en enda kö. Doug Lea designade paketet java.util.concurrent och inkluderade Lock som en fundamental byggsten.
Låsning hanterar åtkomst via en intern statusflagga och väntekö. När en tråd anropar lock() kontrollerar mekanismen om låsningen är ledig och låser den antingen eller placerar tråden i kön tills frigivning.
Grunden för varje låsning är en atomisk jämför-och-sätt (CAS)-operation. Vid anrop av lock() försöker tråden atomiskt sätta upptagenhetsflaggan. Om flaggan redan är satt blockeras tråden. Vid unlock() återställs flaggan och en av de väntande trådarna väcks.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// kritisk sektion
println("Tråden ${Thread.currentThread().name} arbetar")
} finally {
lock.unlock()
}
}
ReentrantLock använder internt en dubbelriktad kö (CLH lock queue), där varje väntande tråd representeras av en nod. När låsningen frigörs väcks huvudnoden i kön. Fair-läge (rättvist) garanterar FIFO-ordning, medan unfair tillåter en ny tråd att låsa före väntande för ökad genomströmning.
I den moderna Java-stacken finns flera implementeringar av låsningar, var och en optimerad för specifika scenarier. Valet av rätt låsning påverkar direkt prestanda och tillförlitlighet hos en flertrådad applikation.
ReentrantLock — den grundläggande och mest använda implementeringen av Lock. Den stöder återlåsning av samma tråd: om en tråd redan har låsningen blockerar inte ett upprepat lock()-anrop. Detta förhindrar deadlock vid rekursiva anrop.
ReadWriteLock delar upp låsningar i två lägen: läsning och skrivning. Flera trådar kan samtidigt hålla läsningslåsningen, men skrivning kräver exklusiv åtkomst. Detta ökar prestandan avsevärt vid frekvent läsning och sällsynt skrivning.
StampedLock — den senaste implementeringen, som dök upp i Java 8. Den stöder tre lägen: skrivning, läsning och optimistisk läsning. Optimistisk läsning blockerar inte andra trådar och validerar data efter läsning, vilket ger 10-20% prestandaökning jämfört med ReadWriteLock.
| Låsning | Java-version | Lägen | Prestanda |
|---|---|---|---|
| ReentrantLock | Java 5 | exklusiv | hög |
| ReadWriteLock | Java 5 | läsning + skrivning | medel |
| StampedLock | Java 8 | läsning + skrivning + optimistic | mycket hög |
ReentrantLock — den populäraste implementeringen av Lock, som erbjuder en rad möjligheter som inte finns i synchronized. Förståelse av dess egenskaper är nödvändig för effektivt arbete med flertrådighet.
Konstruktorn för ReentrantLock accepterar parametern fair. Vid true garanterar låsningen FIFO-ordning för åtkomst, vid false är låsning av en ny tråd före väntande möjlig. Rättvist läge förhindrar svält men minskar genomströmningen med 10-20% på grund av extra overhead för köhantering.
Till skillnad från synchronized stöder ReentrantLock tryLock med timeout. Om låsningen inte kan erhållas inom angiven tid fortsätter tråden exekvering istället för att blockeras oändligt. Metoden lockInterruptibly gör det möjligt att avbryta en väntande tråd via Thread.interrupt().
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Låsning erhållen")
} finally {
lock.unlock()
}
} else {
println("Det gick inte att erhålla låsning")
}
}
ReentrantLock stöder flera villkorsvariabler via metoden newCondition(). Varje Condition har sin egen väntekö, vilket möjliggör organisering av komplexa väckningsscenarier. Metoderna await() och signal() har ersatt wait() och notify() från synchronized-block, men med stöd för flera köer.
ReadWriteLock och StampedLock löser problemet med åtkomstoptimering när läsoperationer dominerar över skrivning. De är betydligt effektivare än ReentrantLock i scenarier där läsning sker oftare än skrivning.
ReadWriteLock-gränssnittet innehåller två metoder: readLock() och writeLock(). Läslåsningen kan hållas av flera trådar samtidigt, skrivlåsningen — endast av en. Ett typiskt exempel — en trådsäker cache: många trådar läser data och endast en uppdaterar dem periodiskt.
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 lägger till ett tredje läge — tryOptimisticRead. Detta läge blockerar inte andra trådar, utan kommer bara ihåg en stämpel (stamp) av tillståndet. Efter läsning anropar utvecklaren validate(stamp) för att kontrollera om data ändrades under läsningen. Om data ändrades måste operationen upprepas.
I mobila applikationer används låsningar för att koordinera åtkomst till delad data mellan trådar. Deras användning kräver dock särskild försiktighet på grund av begränsade enhetsresurser och behovet av att bibehålla gränssnittets responsivitet.
På Android är ReentrantLock användbart vid arbete med Room, cacheminnen och filer. Det är viktigt att komma ihåg: lås aldrig på huvudtråden. För asynkron kod är korutiner och Mutex från kotlinx.coroutines att föredra, som inte blockerar tråden utan pausar korutinen.
I iOS används standard Lock från NSLock mer sällan — utvecklare föredrar DispatchQueue med barrier-flaggor eller operativa låsningar os_unfair_lock. Swift 5.7+ tillhandahåller moderna synkroniseringsmekanismer via actors, som automatiskt skyddar tillståndet.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
För att undvika deadlock, följ enhetlig låsningsordning för alla låsningar i projektet. Använd tryLock med timeout istället för lock() överallt där långvarig blockering är möjlig. Överväg att använda Lock-Free-algoritmer (AtomicReference, ConcurrentHashMap) istället för traditionella låsningar.
Användning av Lock kräver disciplin och efterlevnad av flera regler som förhindrar deadlock och prestandaförsämring. Denna praxis har utvecklats av Java-communityn under 20 års användning av paketet java.util.concurrent.
Det viktigaste mönstret — lock i finally. Oavsett om den kritiska sektionen avslutades framgångsrikt eller med ett undantag, måste låsningen frigöras. Detta garanterar att andra trådar inte blockeras för evigt på grund av ett enda fel. I Kotlin löses detta mönster elegant genom tillägget withLock.
Den kritiska sektionen bör vara maximalt kort. Utför aldrig inmatning-utmatning, nätverksförfrågningar eller långa beräkningar inom låsningen. Om du behöver läsa data från servern, hämta dem först, lås sedan endast för att uppdatera delat tillstånd. Detta minskar konkurrensen och ökar systemets genomströmning.
För att förhindra deadlock vid arbete med flera Lock, fastställ global låsningsordning i hela projektet. Om först lockA, sedan lockB låses — måste varje omvänd sekvens förbjudas av code review-regler. För automatisk kontroll, använd statiska analysatorer som SpotBugs och IntelliJ Inspections.
Vanliga frågor
Lock — explicit gränssnitt med möjlighet till timeout och avbrytbar väntan. synchronized låser och låser upp automatiskt, men tillåter inte användning av tryLock, lockInterruptibly och flera Condition. Lock är flexiblare, men kräver manuell frigivning i finally.
Rättvis låsning garanterar FIFO-ordning för åtkomst: tråden som väntat längst får först låsningen. Orättvis låsning kan ge åtkomst till en ny tråd som kringgår kön, vilket ökar genomströmningen men kan orsaka svält för väntande trådar.
Följ fast ordning för låsning av alla lås, använd tryLock med timeout istället för ovillkorlig lock och minimera antalet samtidigt hållna låsningar. Tillämpning av Lock-Free-datastrukturer minskar också risken för deadlock.
Condition — motsvarigheten till wait/notify för Lock, som möjliggör organisering av flera oberoende vänteköer. Varje anrop av newCondition() skapar en separat kö, vilket ger mer precis kontroll över väckning av trådar jämfört med synchronizeds enda kö.
För Android med korutiner, använd Mutex från kotlinx.coroutines — det pausar korutinen, inte tråden. För iOS med Swift 5.7+ är actors att föredra, som automatiskt synkroniserar åtkomst till tillståndet. Lämna ReentrantLock för äldre kod och lågnivåscenarier.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också