Lock: co to jest, typy blokad i użycie w synchronizacji

Autor: IT Sectr Opublikowano: 2026-03-19 Czas czytania: 8 min

Lock to mechanizm synchronizacji zapewniający ekskluzywny dostęp do sekcji krytycznych kodu w aplikacjach wielowątkowych. Według Oracle, 2024, interfejs Lock zapewnia bardziej elastyczną kontrolę synchronizacji w porównaniu z tradycyjnymi blokami synchronized, w tym próby przechwycenia z timeoutem i obsługę wielu kolejek oczekiwania.

Najważniejsze

  • Lock — interfejs do jawnego zarządzania blokadami w Javie.
  • ReentrantLock — podstawowa implementacja z obsługą ponownego przechwycenia przez ten sam wątek.
  • ReadWriteLock dzieli blokady na odczyt i zapis w celu zwiększenia wydajności.
  • Deadlock — główne ryzyko przy jednoczesnym użyciu wielu blokad.
  • W przeciwieństwie do synchronized, Lock obsługuje timeouty i przerywalne oczekiwanie.

Co to jest Lock?

Lock to interfejs z pakietu java.util.concurrent.locks, zapewniający jawne operacje przechwytywania i zwalniania w celu synchronizacji dostępu do danych. W przeciwieństwie do synchronized, Lock daje programiście pełną kontrolę nad mechanizmem blokady.

Definicja i rola w synchronizacji

Interfejs Lock pojawił się w Javie 5 jako alternatywa dla wbudowanego mechanizmu synchronized. Główne metody to lock, unlock, tryLock i lockInterruptibly. Blokady umożliwiają zorganizowanie bezpiecznego dostępu do danych w środowisku wielowątkowym, zapobiegając stanom wyścigu i uszkodzeniu danych.

Główna zaleta Lock w porównaniu z synchronized to elastyczność. Programista może spróbować przechwycić blokadę z timeoutem, sprawdzić jej zajętość bez blokowania lub zorganizować wiele kolejek oczekiwania z różnymi priorytetami.

Historia rozwoju

Przed pojawieniem się interfejsu Lock w Javie 5 jedynym sposobem synchronizacji był synchronized, który cierpiał na ograniczenia: brak timeoutów, niemożność przerwania oczekiwania i pojedyncza kolejka. Doug Lea zaprojektował pakiet java.util.concurrent, włączając do niego Lock jako fundamentalny blok konstrukcyjny.

Jak działa blokada?

Blokada zarządza dostępem poprzez wewnętrzny flagę stanu i kolejkę oczekiwania. Gdy wątek wywołuje lock(), mechanizm sprawdza, czy blokada jest wolna, i albo ją przechwytuje, albo umieszcza wątek w kolejce do czasu zwolnienia.

Atomowe przechwycenie i zwolnienie

U podstaw każdej blokady leży atomowa operacja porównania i ustawienia (CAS). Przy wywołaniu lock() wątek próbuje atomowo ustawić flagę zajętości. Jeśli flaga jest już ustawiona, wątek zostaje zablokowany. Przy unlock() flaga jest resetowana i budzony jest jeden z oczekujących wątków.

kotlin
import java.util.concurrent.locks.ReentrantLock

val lock = ReentrantLock()

fun performTask() {
    lock.lock()
    try {
        // sekcja krytyczna
        println("Działa wątek ${Thread.currentThread().name}")
    } finally {
        lock.unlock()
    }
}

Kolejka oczekiwania i budzenie

ReentrantLock wewnętrznie używa dwukierunkowej kolejki (CLH lock queue), gdzie każdy oczekujący wątek jest reprezentowany przez węzeł. Gdy blokada zostaje zwolniona, budzony jest główny węzeł kolejki. Tryb fair (sprawiedliwy) gwarantuje kolejność FIFO, a unfair pozwala na przechwycenie przez nowy wątek przed oczekującymi w celu zwiększenia przepustowości.

Podstawowe rodzaje blokad

W nowoczesnym stosie Javy istnieje kilka implementacji blokad, z których każda jest zoptymalizowana pod konkretne scenariusze. Wybór odpowiedniej blokady bezpośrednio wpływa na wydajność i niezawodność aplikacji wielowątkowej.

ReentrantLock

ReentrantLock — podstawowa i najczęściej używana implementacja Lock. Obsługuje ponowne przechwycenie przez ten sam wątek: jeśli wątek już posiada blokadę, ponowne wywołanie lock() go nie blokuje. Zapobiega to deadlockowi przy wywołaniach rekurencyjnych.

ReentrantReadWriteLock

ReadWriteLock dzieli blokady na dwa tryby: odczyt i zapis. Wiele wątków może jednocześnie utrzymywać blokadę odczytu, ale zapis wymaga ekskluzywnego dostępu. Znacznie zwiększa to wydajność przy częstym odczycie i rzadkim zapisie.

StampedLock

StampedLock — najnowsza implementacja, która pojawiła się w Javie 8. Obsługuje trzy tryby: zapis, odczyt i optymistyczny odczyt. Optymistyczny odczyt nie blokuje innych wątków i sprawdza ważność danych po odczycie, co daje wzrost wydajności o 10-20% w porównaniu z ReadWriteLock.

BlokadaWersja JavaTrybyWydajność
ReentrantLockJava 5ekskluzywnywysoka
ReadWriteLockJava 5odczyt + zapisśrednia
StampedLockJava 8odczyt + zapis + optimisticbardzo wysoka

ReentrantLock i jego cechy

ReentrantLock — najpopularniejsza implementacja Lock, zapewniająca szereg możliwości niedostępnych w synchronized. Zrozumienie jej cech jest niezbędne do efektywnej pracy z wielowątkowością.

Sprawiedliwość blokady (fairness)

Konstruktor ReentrantLock przyjmuje parametr fair. Przy true blokada gwarantuje kolejność FIFO dostępu, przy false możliwe jest przechwycenie przez nowy wątek przed oczekującymi. Tryb sprawiedliwy zapobiega zagłodzeniu, ale obniża przepustowość o 10-20% z powodu dodatkowych narzutów na utrzymanie kolejki.

Timeouty i przerywalne oczekiwanie

W przeciwieństwie do synchronized, ReentrantLock obsługuje tryLock z timeoutem. Jeśli nie udało się przechwycić blokady w określonym czasie, wątek kontynuuje wykonanie, zamiast blokować się w nieskończoność. Metoda lockInterruptibly pozwala przerwać oczekujący wątek poprzez Thread.interrupt().

kotlin
val lock = ReentrantLock()

fun tryTask() {
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            println("Blokada przechwycona")
        } finally {
            lock.unlock()
        }
    } else {
        println("Nie udało się przechwycić blokady")
    }
}

Warunki (Conditions)

ReentrantLock obsługuje wiele zmiennych warunkowych poprzez metodę newCondition(). Każda Condition ma własną kolejkę oczekiwania, co pozwala organizować złożone scenariusze budzenia. Metody await() i signal() zastąpiły wait() i notify() z bloków synchronized, ale z obsługą wielu kolejek.

ReadWriteLock i StampedLock

ReadWriteLock i StampedLock rozwiązują zadanie optymalizacji dostępu przy przewadze operacji odczytu nad zapisem. Są znacznie wydajniejsze niż ReentrantLock w scenariuszach, gdzie odczyt występuje częściej niż zapis.

ReadWriteLock w praktyce

Interfejs ReadWriteLock zawiera dwie metody: readLock() i writeLock(). Blokada odczytu może być utrzymywana przez wiele wątków jednocześnie, blokada zapisu — tylko przez jeden. Typowym przykładem jest bezpieczny wątkowo cache: wiele wątków czyta dane, a tylko jeden okresowo je aktualizuje.

kotlin
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 i optymistyczny odczyt

StampedLock dodaje trzeci tryb — tryOptimisticRead. Ten tryb nie blokuje innych wątków, a jedynie zapamiętuje znacznik (stamp) stanu. Po odczycie programista wywołuje validate(stamp), sprawdzając, czy dane nie zmieniły się podczas odczytu. Jeśli dane się zmieniły, operację należy powtórzyć.

Blokady w programowaniu mobilnym

W aplikacjach mobilnych blokady są używane do koordynacji dostępu do współdzielonych danych między wątkami. Jednak ich stosowanie wymaga szczególnej ostrożności ze względu na ograniczone zasoby urządzenia i konieczność zachowania responsywności interfejsu.

Blokady w Android (Kotlin)

Na Androidzie ReentrantLock jest przydatny przy pracy z Room, cache'ami i plikami. Ważne jest, aby nigdy nie przechwytywać blokady na głównym wątku. Do kodu asynchronicznego preferowane są korutyny i Mutex z kotlinx.coroutines, które nie blokują wątku, a zawieszają korutynę.

Blokady w iOS (Swift)

W iOS standardowy Lock z NSLock jest rzadziej używany — programiści preferują DispatchQueue z flagami barrier lub blokady operacyjne os_unfair_lock. Swift 5.7+ zapewnia nowoczesne mechanizmy synchronizacji poprzez actors, które automatycznie chronią stan.

swift
import Foundation

actor DataStore {
    private var items: [String] = []

    func add(_ item: String) {
        items.append(item)
    }

    func getAll() -> [String] {
        items
    }
}

Zalecenia dotyczące unikania deadlock

Aby uniknąć deadlocka, przestrzegaj jednolitego porządku przechwytywania wszystkich blokad w projekcie. Używaj tryLock z timeoutem zamiast lock() wszędzie tam, gdzie możliwe jest długotrwałe blokowanie. Rozważ zastosowanie algorytmów Lock-Free (AtomicReference, ConcurrentHashMap) zamiast tradycyjnych blokad.

Najlepsze praktyki pracy z Lock

Stosowanie Lock wymaga dyscypliny i przestrzegania kilku zasad, które zapobiegają deadlockowi i spadkowi wydajności. Te praktyki zostały wypracowane przez społeczność Java przez 20 lat używania pakietu java.util.concurrent.

Zwalnianie w finally

Najważniejszy wzorzec — lock w finally. Niezależnie od tego, czy sekcja krytyczna zakończyła się sukcesem czy wyjątkiem, blokada musi zostać zwolniona. Gwarantuje to, że inne wątki nie zostaną zablokowane na zawsze z powodu jednego błędu. W Kotlin ten wzorzec jest elegancko rozwiązany poprzez rozszerzenie withLock.

Minimalizacja czasu utrzymywania

Sekcja krytyczna powinna być maksymalnie krótka. Nigdy nie wykonuj wewnątrz blokady operacji wejścia-wyjścia, zapytań sieciowych ani długotrwałych obliczeń. Jeśli potrzebujesz odczytać dane z serwera, najpierw je pobierz, a następnie przechwyć blokadę tylko do aktualizacji współdzielonego stanu. Zmniejsza to rywalizację i zwiększa przepustowość systemu.

Jednolity porządek przechwytywania

Aby zapobiec deadlockowi przy pracy z wieloma Lock, ustal globalny porządek przechwytywania w całym projekcie. Jeśli najpierw przechwytywany jest lockA, a następnie lockB — każda odwrotna sekwencja powinna być zabroniona przez zasady code review. Do automatycznego sprawdzania używaj statycznych analizatorów, takich jak SpotBugs i IntelliJ Inspections.

Często zadawane pytania

Czym różni się Lock od synchronized?

Lock — jawny interfejs z możliwością timeoutu i przerywalnego oczekiwania. synchronized automatycznie przechwytuje i zwalnia monitor, ale nie pozwala na użycie tryLock, lockInterruptibly i wielu Condition. Lock jest bardziej elastyczny, ale wymaga ręcznego zwalniania w finally.

Co to jest sprawiedliwa blokada (fair lock)?

Sprawiedliwa blokada gwarantuje kolejność FIFO dostępu: wątek, który czeka najdłużej, pierwszy otrzymuje blokadę. Niesprawiedliwa blokada może oddać dostęp nowemu wątkowi z pominięciem kolejki, co zwiększa przepustowość, ale może spowodować zagłodzenie oczekujących wątków.

Jak uniknąć deadlocka przy użyciu Lock?

Przestrzegaj stałego porządku przechwytywania wszystkich blokad, używaj tryLock z timeoutem zamiast bezwarunkowego lock i minimalizuj liczbę jednocześnie utrzymywanych blokad. Stosowanie struktur danych Lock-Free również zmniejsza ryzyko deadlocka.

Co to jest Condition w Lock?

Condition — odpowiednik wait/notify dla Lock, umożliwiający zorganizowanie wielu niezależnych kolejek oczekiwania. Każde wywołanie newCondition() tworzy oddzielną kolejkę, co daje dokładniejszą kontrolę nad budzeniem wątków w porównaniu z pojedynczą kolejką synchronized.

Którą blokadę wybrać dla aplikacji mobilnej?

Dla Androida z korutynami używaj Mutex z kotlinx.coroutines — zawiesza on korutynę, a nie blokuje wątku. Dla iOS z Swift 5.7+ preferowane są actors, które automatycznie synchronizują dostęp do stanu. ReentrantLock pozostaw dla starszego kodu i niskopoziomowych scenariuszy.

Podsumowanie

  • Lock — interfejs jawnego zarządzania blokadami z java.util.concurrent.locks.
  • ReentrantLock — podstawowa implementacja z obsługą ponownego przechwycenia i sprawiedliwości.
  • ReadWriteLock dzieli blokady na odczyt i zapis dla scenariuszy read-heavy.
  • StampedLock dodaje optymistyczny odczyt dla maksymalnej wydajności.
  • Timeouty i Condition — kluczowe zalety Lock przed synchronized.
  • Deadlock zapobiega się stałym porządkiem przechwytywania i użyciem tryLock.
  • W programowaniu mobilnym zalecane są korutyny (Android) i actors (iOS).

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również