Lock je synchronizační mechanismus zajišťující exkluzivní přístup ke kritickým sekcím kódu ve vícevláknových aplikacích. Podle Oracle, 2024 poskytuje rozhraní Lock flexibilnější kontrolu synchronizace ve srovnání s tradičními synchronized bloky, včetně pokusů o zámek s timeoutem a podpory více front čekání.
Hlavní body
Lock je rozhraní z balíčku java.util.concurrent.locks, které poskytuje explicitní operace uzamčení a odemčení pro synchronizaci přístupu k datům. Na rozdíl od synchronized dává Lock vývojáři plnou kontrolu nad mechanismem zámku.
Rozhraní Lock se objevilo v Javě 5 jako alternativa k vestavěnému mechanismu synchronized. Hlavní metody jsou lock, unlock, tryLock a lockInterruptibly. Zámky umožňují organizovat bezpečný přístup k datům ve vícevláknovém prostředí a zabraňují race condition a poškození dat.
Hlavní výhoda Locku oproti synchronized je flexibilita. Vývojář může zkusit získat zámek s timeoutem, zkontrolovat jeho obsazenost bez blokování nebo organizovat více front čekání s různými prioritami.
Před objevením rozhraní Lock v Javě 5 byl jediným způsobem synchronizace synchronized, který trpěl omezeními: nedostatek timeoutů, nemožnost přerušení čekání a jediná fronta. Doug Lea navrhl balíček java.util.concurrent a zahrnul Lock jako základní stavební blok.
Zámek řídí přístup prostřednictvím interního stavového příznaku a fronty čekání. Když vlákno zavolá lock(), mechanismus zkontroluje, zda je zámek volný, a buď jej zamkne, nebo umístí vlákno do fronty až do uvolnění.
Základem každého zámku je atomická operace porovnání a nastavení (CAS). Při volání lock() se vlákno pokusí atomicky nastavit příznak obsazenosti. Pokud je příznak již nastaven, vlákno je blokováno. Při unlock() je příznak resetován a jedno z čekajících vláken je probuzeno.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// kritická sekce
println("Pracuje vlákno ${Thread.currentThread().name}")
} finally {
lock.unlock()
}
}
ReentrantLock interně používá obousměrnou frontu (CLH lock queue), kde každé čekající vlákno je reprezentováno uzlem. Když je zámek uvolněn, hlavní uzel fronty je probuzen. Režim fair (spravedlivý) garantuje pořadí FIFO, zatímco unfair umožňuje uzamčení novým vláknem před čekajícími pro zvýšení propustnosti.
V moderním Java stacku existuje několik implementací zámků, každá optimalizovaná pro konkrétní scénáře. Výběr správného zámku přímo ovlivňuje výkon a spolehlivost vícevláknové aplikace.
ReentrantLock — základní a nejčastěji používaná implementace Lock. Podporuje opětovné uzamčení stejným vláknem: pokud vlákno již zámek vlastní, opakované volání lock() jej neblokuje. Tím se předchází deadlocku při rekurzivních voláních.
ReadWriteLock rozděluje zámky na dva režimy: čtení a zápis. Několik vláken může současně držet zámek pro čtení, ale zápis vyžaduje exkluzivní přístup. To výrazně zvyšuje výkon při častém čtení a vzácném zápisu.
StampedLock — nejnovější implementace, která se objevila v Javě 8. Podporuje tři režimy: zápis, čtení a optimistické čtení. Optimistické čtení neblokuje ostatní vlákna a po přečtení kontroluje platnost dat, což poskytuje 10-20% nárůst výkonu oproti ReadWriteLock.
| Zámek | Verze Java | Režimy | Výkon |
|---|---|---|---|
| ReentrantLock | Java 5 | exkluzivní | vysoký |
| ReadWriteLock | Java 5 | čtení + zápis | střední |
| StampedLock | Java 8 | čtení + zápis + optimistic | velmi vysoký |
ReentrantLock — nejoblíbenější implementace Lock, která poskytuje řadu možností nedostupných v synchronized. Porozumění jeho vlastnostem je nezbytné pro efektivní práci s vícevláknovostí.
Konstruktor ReentrantLock přijímá parametr fair. Při true zámek garantuje pořadí FIFO přístupu, při false je možné uzamčení novým vláknem před čekajícími. Spravedlivý režim zabraňuje hladovění, ale snižuje propustnost o 10-20% kvůli dodatečným režiím na údržbu fronty.
Na rozdíl od synchronized podporuje ReentrantLock tryLock s timeoutem. Pokud zámek nelze získat v určeném čase, vlákno pokračuje v provádění místo nekonečného blokování. Metoda lockInterruptibly umožňuje přerušit čekající vlákno pomocí Thread.interrupt().
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Zámek získán")
} finally {
lock.unlock()
}
} else {
println("Nepodařilo se získat zámek")
}
}
ReentrantLock podporuje několik podmínkových proměnných prostřednictvím metody newCondition(). Každá Condition má vlastní frontu čekání, což umožňuje organizovat složité scénáře probuzení. Metody await() a signal() nahradily wait() a notify() z synchronized bloků, ale s podporou více front.
ReadWriteLock a StampedLock řeší problém optimalizace přístupu při převaze operací čtení nad zápisem. Jsou výrazně účinnější než ReentrantLock ve scénářích, kde čtení probíhá častěji než zápis.
Rozhraní ReadWriteLock obsahuje dvě metody: readLock() a writeLock(). Zámek čtení může být držen několika vlákny současně, zámek zápisu — pouze jedním. Typickým příkladem je thread-safe cache: mnoho vláken čte data a pouze jedno je pravidelně aktualizuje.
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 přidává třetí režim — tryOptimisticRead. Tento režim neblokuje ostatní vlákna, pouze si pamatuje razítko (stamp) stavu. Po přečtení vývojář zavolá validate(stamp) a zkontroluje, zda se data během čtení nezměnila. Pokud se data změnila, operaci je třeba opakovat.
V mobilních aplikacích se zámky používají ke koordinaci přístupu ke sdíleným datům mezi vlákny. Jejich použití však vyžaduje zvláštní opatrnost kvůli omezeným zdrojům zařízení a potřebě zachovat odezvu rozhraní.
Na Androidu je ReentrantLock užitečný při práci s Room, cache a soubory. Je důležité si pamatovat: nikdy nezamykejte na hlavním vlákně. Pro asynchronní kód jsou preferovány korutiny a Mutex z kotlinx.coroutines, které neblokují vlákno, ale pozastavují korutinu.
V iOS se standardní Lock z NSLock používá méně často — vývojáři preferují DispatchQueue s barrier příznaky nebo operační zámky os_unfair_lock. Swift 5.7+ poskytuje moderní synchronizační mechanismy prostřednictvím actors, které automaticky chrání stav.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
Abyste předešli deadlocku, dodržujte jednotné pořadí uzamykání všech zámků v projektu. Používejte tryLock s timeoutem místo lock() všude tam, kde je možné dlouhodobé blokování. Zvažte použití Lock-Free algoritmů (AtomicReference, ConcurrentHashMap) místo tradičních zámků.
Použití Lock vyžaduje disciplínu a dodržování několika pravidel, která zabraňují deadlocku a poklesu výkonu. Tyto postupy byly vyvinuty komunitou Java během 20 let používání balíčku java.util.concurrent.
Nejdůležitější vzor — lock v finally. Bez ohledu na to, zda kritická sekce skončila úspěšně nebo výjimkou, zámek musí být uvolněn. To zaručuje, že ostatní vlákna nebudou navždy zablokována kvůli jedné chybě. V Kotlin je tento vzor elegantně vyřešen pomocí rozšíření withLock.
Kritická sekce by měla být maximálně krátká. Nikdy neprovádějte uvnitř zámku vstupně-výstupní operace, síťové požadavky nebo dlouhé výpočty. Pokud potřebujete číst data ze serveru, nejprve je získejte, poté zamkněte pouze pro aktualizaci sdíleného stavu. To snižuje konkurenci a zvyšuje propustnost systému.
Pro zabránění deadlocku při práci s více Lock stanovte globální pořadí uzamykání v celém projektu. Pokud se nejprve zamyká lockA, poté lockB — každé obrácené pořadí musí být zakázáno pravidly code review. Pro automatickou kontrolu používejte statické analyzátory, jako jsou SpotBugs a IntelliJ Inspections.
Často kladené otázky
Lock — explicitní rozhraní s možností timeoutu a přerušitelného čekání. synchronized automaticky zamyká a odemyká monitor, ale neumožňuje použití tryLock, lockInterruptibly a více Condition. Lock je flexibilnější, ale vyžaduje ruční uvolnění v finally.
Spravedlivý zámek garantuje pořadí FIFO přístupu: vlákno, které čeká nejdéle, dostane zámek jako první. Nespravedlivý zámek může poskytnout přístup novému vláknu obcházejícímu frontu, což zvyšuje propustnost, ale může způsobit hladovění čekajících vláken.
Dodržujte pevné pořadí uzamykání všech zámků, používejte tryLock s timeoutem místo bezpodmínečného lock a minimalizujte počet současně držených zámků. Použití Lock-Free datových struktur také snižuje riziko deadlocku.
Condition — analog wait/notify pro Lock, umožňující organizovat několik nezávislých front čekání. Každé volání newCondition() vytváří samostatnou frontu, což poskytuje přesnější kontrolu nad probouzením vláken ve srovnání s jedinou frontou synchronized.
Pro Android s korutinami používejte Mutex z kotlinx.coroutines — ten pozastavuje korutinu, neblokuje vlákno. Pro iOS s Swift 5.7+ jsou preferovány actors, které automaticky synchronizují přístup ke stavu. ReentrantLock ponechte pro starší kód a nízkoúrovňové scénáře.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také