Lock је механизам синхронизације који обезбеђује ексклузивни приступ критичним секцијама кода у вишегрупим апликацијама. Према Oracle, 2024, Lock интерфејс пружа флексибилнију контролу синхронизације у поређењу са традиционалним synchronized блоковима, укључујући покушаје преузимања са тајмаутом и подршку за више редова чекања.
Главне тачке
Lock је интерфејс из пакета java.util.concurrent.locks који пружа експлицитне операције преузимања и ослобађања ради синхронизације приступа подацима. За разлику од synchronized, Lock даје програмеру потпуну контролу над механизмом блокирања.
Lock интерфејс се појавио у Java 5 као алтернатива уграђеном synchronized механизму. Главне методе су lock, unlock, tryLock и lockInterruptibly. Блокирања омогућавају организовање безбедног приступа подацима у вишегрупом окружењу, спречавајући стања трке и оштећење података.
Главна предност Lock у односу на synchronized је флексибилност. Програмер може покушати да преузме блокирање са тајмаутом, провери његову заузетост без блокирања или организовати више редова чекања са различитим приоритетима.
Пре појаве Lock интерфејса у Java 5, једини начин синхронизације био је synchronized, који је патио од ограничења: недостатак тајмаута, немогућност прекида чекања и један ред. Даг Ли је дизајнирао пакет java.util.concurrent, укључивши Lock као основни градивни блок.
Блокирање управља приступом кроз интерну заставицу стања и ред чекања. Када нит позове lock(), механизам проверава да ли је блокирање слободно и или га преузима или смешта нит у ред до ослобађања.
У основи сваког блокирања лежи атомска операција поређења и постављања (CAS). При позиву lock(), нит покушава атомски да постави заставицу заузетости. Ако је заставица већ постављена, нит се блокира. При unlock(), заставица се ресетује и буди се једна од нити у чекању.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// критична секција
println("Ради нит ${Thread.currentThread().name}")
} finally {
lock.unlock()
}
}
ReentrantLock интерно користи двострани ред (CLH lock queue), где је свака нит у чекању представљена чвором. Када се блокирање ослободи, главни чвор реда се буди. Fair (поштени) режим гарантује FIFO редослед, а unfair дозвољава преузимање од нове нити пре оних у чекању ради повећања пропусности.
У модерном Java стек-у постоји неколико имплементација блокирања, свака оптимизована за конкретне сценарије. Избор правог блокирања директно утиче на перформансе и поузданост вишегрупе апликације.
ReentrantLock — основна и најчешће коришћена имплементација Lock-а. Подржава поновно преузимање од исте нити: ако нит већ поседује блокирање, поновни позив lock() је не блокира. Ово спречава deadlock при рекурзивним позивима.
ReadWriteLock раздваја блокирања на два режима: читање и писање. Више нити може истовремено држати блокирање читања, али писање захтева ексклузивни приступ. Ово значајно повећава перформансе при честом читању и ретком писању.
StampedLock — најновија имплементација, која се појавила у Java 8. Подржава три режима: писање, читање и оптимистичко читање. Оптимистичко читање не блокира друге нити и проверава валидност података након читања, што даје повећање перформанси од 10-20% у поређењу са ReadWriteLock.
| Блокирање | Java верзија | Режими | Перформансе |
|---|---|---|---|
| ReentrantLock | Java 5 | ексклузивни | високе |
| ReadWriteLock | Java 5 | читање + писање | средње |
| StampedLock | Java 8 | читање + писање + optimistic | врло високе |
ReentrantLock — најпопуларнија имплементација Lock-а, која пружа низ могућности недоступних у synchronized. Разумевање његових карактеристика је неопходно за ефикасан рад са вишегрупошћу.
Конструктор ReentrantLock прихвата параметар fair. При true блокирање гарантује FIFO редослед приступа, при false је могуће преузимање од нове нити пре оних у чекању. Поштени режим спречава гладовање, али смањује пропусност за 10-20% због додатних трошкова одржавања реда.
За разлику од synchronized, ReentrantLock подржава tryLock са тајмаутом. Ако блокирање није могло бити преузето у одређеном времену, нит наставља извршење уместо да се блокира бесконачно. Метода lockInterruptibly омогућава прекид нити у чекању путем Thread.interrupt().
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Блокирање преузето")
} finally {
lock.unlock()
}
} else {
println("Није успело преузимање блокирања")
}
}
ReentrantLock подржава више условних променљивих кроз методу newCondition(). Сваки Condition има сопствени ред чекања, што омогућава организовање сложених сценарија буђења. Методе await() и signal() замениле су wait() и notify() из synchronized блокова, али са подршком за више редова.
ReadWriteLock и StampedLock решавају проблем оптимизације приступа када операције читања преовлађују над писањем. Они су значајно ефикаснији од ReentrantLock-а у сценаријима где се читање дешава чешће од писања.
ReadWriteLock интерфејс садржи две методе: readLock() и writeLock(). Блокирање читања може држати више нити истовремено, блокирање писања — само једна. Типичан пример — безбедни кеш за нити: више нити чита податке, а само једна их повремено ажурира.
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 додаје трећи режим — tryOptimisticRead. Овај режим не блокира друге нити, већ само памти печат (stamp) стања. Након читања, програмер позива validate(stamp) да провери да ли су се подаци променили током читања. Ако су се подаци променили, операција се мора поновити.
У мобилним апликацијама, блокирања се користе за координацију приступа заједничким подацима између нити. Међутим, њихова примена захтева посебну опрезност због ограничених ресурса уређаја и потребе за одржавањем одзивности интерфејса.
На Android-у, ReentrantLock је користан при раду са Room, кешевима и датотекама. Важно је запамтити: никада не преузимајте блокирање на главној нити. За асинхрони код, пожељније су корутине и Mutex из kotlinx.coroutines, који не блокирају нит већ суспендују корутину.
У iOS-у, стандардни Lock из NSLock се ређе користи — програмери преферирају DispatchQueue са barrier заставицама или операциона блокирања os_unfair_lock. Swift 5.7+ пружа модерне механизме синхронизације кроз actors, који аутоматски штите стање.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
Да бисте избегли deadlock, поштујте јединствени редослед преузимања свих блокирања у пројекту. Користите tryLock са тајмаутом уместо lock() свуда где је могуће дуготрајно блокирање. Размотрите примену Lock-Free алгоритама (AtomicReference, ConcurrentHashMap) уместо традиционалних блокирања.
Примена Lock захтева дисциплину и поштовање неколико правила која спречавају deadlock и пад перформанси. Ове праксе су развијене од стране Java заједнице током 20 година коришћења пакета java.util.concurrent.
Најважнији образац — lock у finally. Без обзира да ли се критична секција завршила успешно или изузетком, блокирање мора бити ослобођено. Ово гарантује да друге нити неће бити заувек блокиране због једне грешке. У Kotlin-у, овај образац је елегантно решен кроз екстензију withLock.
Критична секција треба да буде максимално кратка. Никада не извршавајте унутар блокирања улазно-излазне операције, мрежне захтеве или дуготрајна израчунавања. Ако треба да прочитате податке са сервера, прво их добијте, а затим преузмите блокирање само за ажурирање заједничког стања. Ово смањује конкуренцију и повећава пропусност система.
За спречавање deadlock-а при раду са више Lock-ова, утврдите глобални редослед преузимања у целом пројекту. Ако се прво преузима lockA, затим lockB — сваки обрнути редослед мора бити забрањен правилима code review-а. За аутоматску проверу користите статичке анализаторе попут SpotBugs и IntelliJ Inspections.
Често постављана питања
Lock — експлицитни интерфејс са могућношћу тајмаута и прекидног чекања. synchronized аутоматски преузима и ослобађа монитор, али не дозвољава коришћење tryLock, lockInterruptibly и више Condition. Lock је флексибилнији, али захтева ручно ослобађање у finally.
Поштено блокирање гарантује FIFO редослед приступа: нит која најдуже чека прва добија блокирање. Непоштено блокирање може дати приступ новој нити заобилазећи ред, што повећава пропусност, али може изазвати гладовање нити у чекању.
Поштујте фиксни редослед преузимања свих блокирања, користите tryLock са тајмаутом уместо безусловног lock и минимизирајте број истовремено држаних блокирања. Примена Lock-Free структура података такође смањује ризик од deadlock-а.
Condition — аналог wait/notify за Lock, који омогућава организовање више независних редова чекања. Сваки позив newCondition() ствара посебан ред, што даје прецизнију контролу над буђењем нити у поређењу са једним редом synchronized.
За Android са корутинама користите Mutex из kotlinx.coroutines — он суспендује корутину, а не блокира нит. За iOS са Swift 5.7+, пожељни су actors, који аутоматски синхронизују приступ стању. ReentrantLock оставите за стари код и нисконивоске сценарије.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође