Synchronized to wbudowany mechanizm synchronizacji w języku Java, zapewniający ekskluzywny dostęp do sekcji krytycznych kodu. Według Oracle, 2024, modyfikator synchronized gwarantuje, że tylko jeden wątek może wykonywać oznaczoną metodę lub blok w określonym momencie. Mechanizm ten opiera się na monitorach — fundamentalnej koncepcji systemów operacyjnych, zapewniającej poprawne działanie aplikacji wielowątkowych na wszystkich poziomach złożoności.
Najważniejsze
Synchronized to słowo kluczowe w Java, które gwarantuje, że tylko jeden wątek w danej chwili wykonuje chroniony fragment kodu, zapobiegając uszkodzeniu danych przy równoległym dostępie. Pojawiło się w pierwszej wersji Java i pozostaje najprostszym sposobem zapewnienia bezpieczeństwa wątkowego dla programistów na każdym poziomie zaawansowania.
Modyfikator synchronized rozwiązuje dwa zadania: wzajemne wykluczanie (mutual exclusion) i widoczność zmian (visibility). Gdy wątek opuszcza blok synchronized, wszystkie zmiany są gwarantowanie widoczne dla innych wątków wchodzących do bloku zsynchronizowanego na tym samym obiekcie.
Synchronized można zastosować do całej metody lub do dowolnego bloku kodu z określeniem obiektu-monitora. W obu przypadkach JVM wstawia instrukcje monitorenter i monitorexit na poziomie kodu bajtowego.
W aplikacjach wielowątkowych bez synchronizacji występuje stan wyścigu (race condition) — gdy dwa wątki jednocześnie zmieniają te same dane, prowadząc do nieprzewidywalnych rezultatów. Synchronized stał się pierwszym i głównym narzędziem Java do walki z tym problemem, oferując prostą deklaratywną składnię dostępną dla każdego programisty.
Mechanizm synchronized opiera się na koncepcji monitora — wysokopoziomowego prymitywu synchronizacji wbudowanego w każdy obiekt Java. Monitor jest kojarzony z obiektem przy pierwszym użyciu bloku synchronized na nim.
Każdy obiekt w Java ma powiązany monitor. Gdy wątek wchodzi do bloku synchronized, przechwytuje monitor obiektu. Jeśli monitor jest już zajęty przez inny wątek, wątek jest blokowany do momentu zwolnienia. W kodzie bajtowym odpowiada temu para instrukcji monitorenter i monitorexit.
JVM optymalizuje synchronized przez kilka poziomów: biased locking (przesunięta blokada) dla dostępu jednowątkowego, lightweight locking (lekka blokada) przy słabej konkurencji i heavyweight locking (ciężka blokada) przy intensywnej konkurencji z udziałem OS. Te poziomy zwiększają wydajność bez zmiany kodu.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized ustanawia relację happens-before: wszystkie działania w wątku przed wyjściem z bloku synchronized są widoczne dla innego wątku po wejściu do bloku zsynchronizowanego na tym samym obiekcie. Gwarantuje to nie tylko wzajemne wykluczanie, ale także spójność danych dla wszystkich wątków.
Java oferuje dwa sposoby zastosowania synchronized: na poziomie metody i na poziomie bloku. Wybór między nimi wpływa na wydajność i ziarnistość synchronizacji.
Oznaczając metodę modyfikatorem synchronized, automatycznie synchronizujesz ją na bieżącej instancji (dla zwykłej metody) lub na obiekcie Class (dla metody statycznej). To najprostszy sposób zapewnienia wzajemnego wykluczania, ale często jest zbędny, jeśli sekcja krytyczna stanowi tylko małą część metody, a reszta kodu nie wymaga synchronizacji.
Blok synchronized daje precyzyjną kontrolę: określasz obiekt-monitor i synchronizujesz tylko niezbędny fragment kodu, pozostawiając resztę metody poza blokadą. Minimalizuje to czas utrzymywania monitora i zwiększa ogólną wydajność aplikacji w środowisku wielowątkowym, ponieważ inne wątki mogą równolegle wykonywać niepowiązany kod, nie czekając na zwolnienie monitora.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// kod poza sekcją krytyczną - bez synchronizacji
prepareData()
synchronized (lock) {
// tylko ten blok jest chroniony
updateSharedState()
}
// kontynuacja bez blokady
cleanup()
}
}
| Kryterium | Metoda synchronized | Blok synchronized |
|---|---|---|
| Monitor | this (instancja) lub Class | dowolny obiekt |
| Ziarnistość | cała metoda | tylko potrzebny kod |
| Czytelność | wysoka | średnia |
| Wydajność | niższa przy dużej metodzie | wyższa przy małej sekcji krytycznej |
W programowaniu na Androida synchronized jest szeroko stosowany do ochrony SharedPreferences, dostępu do bazy danych i komponentów UI. Jednak jego użycie na głównym wątku jest kategorycznie odradzane ze względu na ryzyko zawieszenia interfejsu.
SharedPreferences w Android zapewnia podstawowe bezpieczeństwo wątkowe, ale przy edycji przez wiele wątków przez Editor może być wymagana zewnętrzna synchronizacja. Blok synchronized z oddzielnym obiektem blokady gwarantuje spójność zmian.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
Główne ograniczenie synchronized na Android — blokowanie wątku. W przeciwieństwie do korutyn z Mutex, synchronized blokuje cały wątek systemowy. Na głównym wątku powoduje to ANR. W nowoczesnym programowaniu na Androida zaleca się zastępowanie synchronized korutynami (suspend Mutex) lub typami atomowymi (AtomicInteger).
Nowoczesny Java i Kotlin oferują kilka alternatyw dla synchronized, z których każda rozwiązuje te same zadania z mniejszymi ograniczeniami lub lepszą wydajnością.
Interfejs Lock z implementacjami ReentrantLock i ReadWriteLock zapewnia limity czasu, oczekiwanie z możliwością przerwania i wiele kolejek Condition. Jest bardziej elastyczny niż synchronized, ale wymaga jawnego zwolnienia w finally, co zwiększa ryzyko błędu przy zapomnianym unlock.
Klasy AtomicInteger, AtomicLong, AtomicReference i inne wykorzystują algorytmy Lock-Free oparte na CAS (Compare-And-Swap). Są znacznie szybsze od synchronized w scenariuszach z umiarkowaną konkurencją, ponieważ nie blokują wątków, lecz wykonują optymistyczne próby ponowne i nie wymagają przełączania kontekstu przez jądro OS.
ThreadLocal zapewnia alternatywne podejście: każda zmienna ThreadLocal jest izolowana w ramach jednego wątku i nie wymaga synchronizacji do odczytu i zapisu. Całkowicie eliminuje to potrzebę stosowania synchronized dla danych, które nie powinny być współdzielone między wątkami. ThreadLocal jest aktywnie używany w frameworkach (Spring, Hibernate) do przechowywania kontekstu transakcji i sesji.
W projektach Kotlin dla Androida alternatywą dla synchronized jest Mutex z kotlinx.coroutines. Nie blokuje on wątku systemu operacyjnego, lecz zawiesza korutynę do momentu zwolnienia blokady — pozwala to efektywnie wykorzystywać wątki puli i uniknąć ANR przy długim oczekiwaniu na zwolnienie zasobu.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
Wydajność synchronized znacznie się zmieniła w ostatnich wersjach Java. Wcześniej uważano go za „ciężki“ mechanizm, ale nowoczesne JVM wyeliminowały większość narzutów dzięki zaawansowanym optymalizacjom kompilatora JIT. Przyjrzyjmy się szczegółowo, jak wirtualna maszyna przyspiesza zsynchronizowany kod w czasie wykonania.
Kompilator JIT JVM stosuje kilka optymalizacji: biased locking eliminuje synchronizację, jeśli blokadę zawsze przechwytuje jeden wątek; lock coarsening łączy sąsiednie bloki synchronized w jeden; lock elimination usuwa synchronizację, jeśli obiekt jest dostępny tylko dla jednego wątku. Te optymalizacje sprawiają, że synchronized jest praktycznie darmowy przy niskiej konkurencji.
JVM określa poziom konkurencji dla każdego obiektu: przy braku konkurencji włączany jest biased locking, przy pojawieniu się drugiego wątku blokada przechodzi w tryb lightweight ze spin-waiting, a dopiero przy długim oczekiwaniu — w heavyweight z systemowym muteksem. Ta eskalacja następuje automatycznie i programista nie musi ręcznie wybierać strategii.
W nowoczesnych benchmarkach (Java 17+) synchronized wykazuje wydajność porównywalną z ReentrantLock przy niskiej i umiarkowanej konkurencji. Przy wysokiej konkurencji Lock może mieć przewagę dzięki bardziej efektywnej kolejce oczekiwania z obsługą timeoutów i przerwań. Dla systemów o wysokim obciążeniu, gdzie konkurencja jest stała, ReentrantLock w trybie fair daje bardziej przewidywalne zachowanie.
Klasy atomowe (AtomicInteger, AtomicReference) pozostają najszybsze dla prostych liczników i flag dzięki implementacji Lock-Free na CAS. W ogóle nie blokują wątków — przy konflikcie operacja jest po prostu powtarzana w pętli. Daje to wzrost wydajności 3-5 razy w porównaniu z synchronized przy operacjach inkrementacji licznika na 4-8 wątkach.
Często zadawane pytania
Synchronized zapewnia zarówno wzajemne wykluczanie, jak i widoczność. Volatile gwarantuje tylko widoczność zmian — zapis do zmiennej volatile jest widoczny dla wszystkich wątków, ale nie zapobiega jednoczesnej modyfikacji, czyli nie chroni przed stanem wyścigu.
Tak, deadlock jest możliwy przy zagnieżdżonej synchronizacji z różną kolejnością monitorów. Na przykład jeden wątek wywołuje synchronized(a) { synchronized(b) }, a drugi — synchronized(b) { synchronized(a) }. Unikaj zagnieżdżonych bloków synchronized lub ustal jednolitą kolejność monitorów.
Monitor to mechanizm synchronizacji powiązany z każdym obiektem Java. Gwarantuje, że tylko jeden wątek wykonuje kod synchronized na danym obiekcie. Monitor obejmuje blokadę, kolejkę oczekiwania i pulę wątków oczekujących na powiadomienie przez wait/notify.
W nowoczesnych wersjach Java (17+) synchronized nie ustępuje Lock pod względem wydajności dzięki optymalizacjom JIT (biased locking, lock coarsening). Lock jest preferowany nie ze względu na szybkość, ale z powodu dodatkowych możliwości: limitów czasu, oczekiwania z możliwością przerwania i wielu Condition.
Statyczna metoda synchronized używa monitora obiektu Class danej klasy, a nie instancji. Oznacza to, że synchronizacja obejmuje wszystkie instancje klasy. Niestatyczne i statyczne metody synchronized używają różnych monitorów i nie blokują się nawzajem.
Podsumowanie
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.
Przeczytaj również