A Lock egy szinkronizációs mechanizmus, amely kizárólagos hozzáférést biztosít a kód kritikus szakaszaihoz a többszálú alkalmazásokban. A Oracle, 2024 szerint a Lock interfész rugalmasabb szinkronizációs vezérlést nyújt a hagyományos synchronized blokkokhoz képest, beleértve a timeoutos zárolási kísérleteket és több várakozási sor támogatását.
Főbb pontok
Lock egy interfész a java.util.concurrent.locks csomagból, amely explicit zárolási és feloldási műveleteket biztosít az adatelérés szinkronizálásához. A synchronized-től eltérően a Lock teljes irányítást ad a fejlesztőnek a zárolási mechanizmus felett.
A Lock interfész a Java 5-ben jelent meg a beépített synchronized mechanizmus alternatívájaként. A fő metódusok: lock, unlock, tryLock és lockInterruptibly. A zárolások lehetővé teszik a biztonságos adatelérés megszervezését többszálú környezetben, megakadályozva a versenyhelyzeteket és az adatsérülést.
A Lock fő előnye a synchronized-hoz képest a rugalmasság. A fejlesztő megpróbálhatja timeouttal zárolni, ellenőrizheti a foglaltságot blokkolás nélkül, vagy több várakozási sort szervezhet különböző prioritásokkal.
A Lock interfész megjelenése előtt a Java 5-ben a szinkronizáció egyetlen módja a synchronized volt, amely korlátokkal küzdött: a timeoutok hiánya, a várakozás megszakításának lehetetlensége és egyetlen sor. Doug Lea megtervezte a java.util.concurrent csomagot, belefoglalva a Lock-ot alapvető építőelemként.
Zárolás egy belső állapotjelzőn és várakozási soron keresztül vezérli a hozzáférést. Amikor egy szál lock()-ot hív, a mechanizmus ellenőrzi, hogy a zárolás szabad-e, és vagy zárolja, vagy a szálat a sorba helyezi a felszabadulásig.
Minden zárolás alapjául egy atomi összehasonlító és beállító (CAS) művelet szolgál. A lock() hívásakor a szál atomi módon próbálja beállítani a foglaltsági jelzőt. Ha a jelző már be van állítva, a szál blokkolódik. Az unlock()-nál a jelző visszaállításra kerül, és a várakozó szálak egyike felébred.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// kritikus szakasz
println("${Thread.currentThread().name} szál működik")
} finally {
lock.unlock()
}
}
ReentrantLock belsőleg kétirányú sort (CLH lock queue) használ, ahol minden várakozó szálat egy csomópont képvisel. Amikor a zárolás felszabadul, a sor fejcsomópontja felébred. A fair (igazságos) mód FIFO sorrendet garantál, míg az unfair lehetővé teszi az új szál zárolását a várakozók előtt a magasabb átvitel érdekében.
A modern Java stack-ben a zárolásoknak több implementációja létezik, mindegyik specifikus forgatókönyvekhez optimalizálva. A megfelelő zárolás kiválasztása közvetlenül befolyásolja a többszálú alkalmazás teljesítményét és megbízhatóságát.
ReentrantLock — a Lock alap és leggyakrabban használt implementációja. Támogatja az azonos szál általi újrazárolást: ha a szál már birtokolja a zárolást, egy ismételt lock() hívás nem blokkolja. Ez megakadályozza a deadlock-ot rekurzív hívásoknál.
A ReadWriteLock két módra osztja a zárolásokat: olvasás és írás. Több szál egyidejűleg tarthatja az olvasási zárolást, de az írás kizárólagos hozzáférést igényel. Ez jelentősen növeli a teljesítményt gyakori olvasás és ritka írás esetén.
StampedLock — a legújabb implementáció, amely a Java 8-ban jelent meg. Három módot támogat: írás, olvasás és optimista olvasás. Az optimista olvasás nem blokkol más szálakat, és az olvasás után ellenőrzi az adatok érvényességét, ami 10-20%-os teljesítménynövekedést eredményez a ReadWriteLock-hoz képest.
| Zárolás | Java verzió | Módok | Teljesítmény |
|---|---|---|---|
| ReentrantLock | Java 5 | kizárólagos | magas |
| ReadWriteLock | Java 5 | olvasás + írás | közepes |
| StampedLock | Java 8 | olvasás + írás + optimistic | nagyon magas |
ReentrantLock — a Lock legnépszerűbb implementációja, amely számos, a synchronized-ben nem elérhető képességet nyújt. Jellemzőinek megértése elengedhetetlen a hatékony többszálú munkához.
A ReentrantLock konstruktora elfogadja a fair paramétert. True esetén a zárolás FIFO sorrendet garantál, false esetén az új szál zárolhat a várakozók előtt. Az igazságos mód megakadályozza az éhezést, de 10-20%-kal csökkenti az átvitelt a sor karbantartásának többletköltségei miatt.
A synchronized-től eltérően a ReentrantLock támogatja a tryLock-ot timeouttal. Ha a zárolás nem szerezhető meg a megadott időn belül, a szál folytatja a végrehajtást ahelyett, hogy végtelenül blokkolna. A lockInterruptibly metódus lehetővé teszi a várakozó szál megszakítását a Thread.interrupt() segítségével.
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Zárolás megszerezve")
} finally {
lock.unlock()
}
} else {
println("Nem sikerült megszerezni a zárolást")
}
}
A ReentrantLock több feltételváltozót támogat a newCondition() metóduson keresztül. Minden Condition saját várakozási sorral rendelkezik, ami lehetővé teszi összetett ébresztési forgatókönyvek megszervezését. Az await() és signal() metódusok felváltották a wait()-et és notify()-t a synchronized blokkokból, de több sor támogatásával.
ReadWriteLock és StampedLock az olvasási műveletek túlsúlyának optimalizálási problémáját oldja meg. Jelentősen hatékonyabbak a ReentrantLock-nál olyan forgatókönyvekben, ahol az olvasás gyakrabban történik, mint az írás.
A ReadWriteLock interfész két metódust tartalmaz: readLock() és writeLock(). Az olvasási zárolást több szál egyidejűleg tarthatja, az írási zárolást csak egy. Tipikus példa — egy szálbiztos gyorsítótár: sok szál olvassa az adatokat, és csak egy frissíti azokat időszakosan.
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 hozzáad egy harmadik módot — tryOptimisticRead. Ez a mód nem blokkol más szálakat, csak megjegyzi az állapot bélyegét (stamp). Olvasás után a fejlesztő meghívja a validate(stamp)-et, hogy ellenőrizze, változtak-e az adatok az olvasás során. Ha az adatok változtak, a műveletet meg kell ismételni.
A mobil alkalmazásokban a zárolásokat a megosztott adatokhoz való hozzáférés összehangolására használják a szálak között. Használatuk azonban különös óvatosságot igényel a korlátozott eszközerőforrások és a felület érzékenységének fenntartása miatt.
Android-on a ReentrantLock hasznos a Room-mal, gyorsítótárakkal és fájlokkal való munkánál. Fontos megjegyezni: soha ne zároljon a főszálon. Az aszinkron kódhoz előnyösebbek a kotlinx.coroutines-ból származó korutinok és Mutex, amelyek nem blokkolják a szálat, hanem felfüggesztik a korutint.
iOS-ben az NSLock standard Lock-ja ritkábban használt — a fejlesztők a DispatchQueue-t barrier jelzőkkel vagy az os_unfair_lock operációs zárolásokat részesítik előnyben. A Swift 5.7+ modern szinkronizációs mechanizmusokat biztosít actors-ön keresztül, amelyek automatikusan védik az állapotot.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
A deadlock elkerülése érdekében tartsa be az összes zárolás egységes sorrendjét a projektben. Használjon tryLock-ot timeouttal a lock() helyett mindenhol, ahol hosszú távú blokkolás lehetséges. Fontolja meg a Lock-Free algoritmusok (AtomicReference, ConcurrentHashMap) alkalmazását a hagyományos zárolások helyett.
A Lock alkalmazása fegyelmet és több szabály betartását igényel, amelyek megakadályozzák a deadlock-ot és a teljesítmény csökkenését. Ezeket a gyakorlatokat a Java közösség fejlesztette ki a java.util.concurrent csomag 20 éves használata során.
A legfontosabb minta — lock a finally-ben. Függetlenül attól, hogy a kritikus szakasz sikeresen vagy kivétellel végződött, a zárolást fel kell szabadítani. Ez garantálja, hogy más szálak nem blokkolódnak örökre egyetlen hiba miatt. Kotlin-ban ez a minta elegánsan megoldott a withLock kiterjesztésen keresztül.
A kritikus szakasznak maximálisan rövidnek kell lennie. Soha ne végezzen bemenet-kimenetet, hálózati kéréseket vagy hosszú számításokat a zároláson belül. Ha adatokat kell olvasnia a szerverről, először szerezze be azokat, majd csak a megosztott állapot frissítéséhez zároljon. Ez csökkenti a versenyt és növeli a rendszer átvitelét.
A deadlock megelőzéséhez több Lock használatakor állapítson meg globális zárolási sorrendet az egész projektben. Ha először lockA, majd lockB zárolódik — minden fordított sorrendet be kell tiltani a code review szabályokkal. Automatikus ellenőrzéshez használjon statikus elemzőket, mint a SpotBugs és IntelliJ Inspections.
Gyakran ismételt kérdések
Lock — explicit interfész timeout és megszakítható várakozás lehetőségével. A synchronized automatikusan zárolja és feloldja a monitort, de nem engedi a tryLock, lockInterruptibly és több Condition használatát. A Lock rugalmasabb, de kézi felszabadítást igényel a finally-ben.
Az igazságos zárolás FIFO sorrendet garantál: a legtovább várakozó szál kapja elsőként a zárolást. Az igazságtalan zárolás a sort megkerülve adhat hozzáférést egy új szálnak, ami növeli az átvitelt, de a várakozó szálak éhezését okozhatja.
Tartsa be az összes Lock rögzített sorrendjét, használjon tryLock-ot timeouttal a feltétel nélküli lock helyett, és minimalizálja az egyidejűleg tartott zárolások számát. A Lock-Free adatstruktúrák alkalmazása is csökkenti a deadlock kockázatát.
Condition — a wait/notify megfelelője a Lock-hoz, amely lehetővé teszi több független várakozási sor megszervezését. Minden newCondition() hívás külön sort hoz létre, ami pontosabb irányítást biztosít a szálak ébresztése felett a synchronized egyetlen sorához képest.
Android-hoz korutinokkal használja a Mutex-et a kotlinx.coroutines-ból — ez felfüggeszti a korutint, nem blokkolja a szálat. iOS-hez Swift 5.7+-szal az actors-ök előnyösebbek, amelyek automatikusan szinkronizálják az állapot elérését. A ReentrantLock-ot hagyja meg örökölt kódhoz és alacsony szintű forgatókönyvekhez.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is