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 ред на достъп: нишката, която чака най-дълго, получава първа блокировката. Несправедливата блокировка може да даде достъп на нова нишка, заобикаляйки опашката, което повишава пропускателната способност, но може да причини гладуване на чакащите нишки.
Спазвайте фиксиран ред на заключване на всички lock, използвайте 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също