Synchronized je vestavěný synchronizační mechanismus v jazyce Java, který zajišťuje exkluzivní přístup ke kritickým sekcím kódu. Podle Oracle, 2024 modifikátor synchronized zaručuje, že pouze jedno vlákno může spouštět označenou metodu nebo blok v určitém okamžiku. Tento mechanismus je založen na monitorech — fundamentálním konceptu operačních systémů, který zajišťuje správnou činnost vícevláknových aplikací na všech úrovních složitosti.
Hlavní body
Synchronized je klíčové slovo v Java, které zaručuje, že pouze jedno vlákno současně spouští chráněnou část kódu, čímž zabraňuje poškození dat při paralelním přístupu. Objevilo se v první verzi Java a zůstává nejjednodušším způsobem zajištění bezpečnosti vláken pro vývojáře všech úrovní.
Modifikátor synchronized řeší dva úkoly: vzájemné vyloučení (mutual exclusion) a viditelnost změn (visibility). Když vlákno opustí synchronized blok, všechny změny jsou zaručeně viditelné pro další vlákna vstupující do bloku synchronizovaného na stejném objektu.
Synchronized lze aplikovat na celou metodu nebo na libovolný blok kódu s určením monitorovacího objektu. V obou případech JVM vkládá instrukce monitorenter a monitorexit na úrovni bytekódu.
Ve vícevláknových aplikacích bez synchronizace vzniká závitové prohledávání (race condition) — když dvě vlákna současně mění stejná data, což vede k nepředvídatelným výsledkům. Synchronized se stal prvním a hlavním nástrojem Java pro boj s tímto problémem, poskytující jednoduchou deklarativní syntaxi přístupnou každému vývojáři.
Mechanismus synchronized je založen na konceptu monitoru — vysokoúrovňového synchronizačního primitivu vestavěného do každého objektu Java. Monitor je k objektu připojen při prvním použití synchronized bloku na něm.
Každý objekt v Java má přidružený monitor. Když vlákno vstoupí do synchronized bloku, převezme monitor objektu. Pokud je monitor již obsazen jiným vláknem, vlákno je blokováno do uvolnění. V bytekódu tomu odpovídá dvojice instrukcí monitorenter a monitorexit.
JVM optimalizuje synchronized prostřednictvím několika úrovní: biased locking (předpojaté zamykání) pro jednovláknový přístup, lightweight locking (lehlé zamykání) při nízké konkurenci a heavyweight locking (těžké zamykání) při intenzivní konkurenci s účastí OS. Tyto úrovně zvyšují výkon bez změny kódu.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized vytváří vztah happens-before: všechny akce ve vlákně před opuštěním synchronized bloku jsou viditelné pro jiné vlákno po vstupu do bloku synchronizovaného na stejném objektu. To zaručuje nejen vzájemné vyloučení, ale také konzistenci dat pro všechna vlákna.
Java nabízí dva způsoby aplikace synchronized: na úrovni metody a na úrovni bloku. Volba mezi nimi ovlivňuje výkon a granularitu synchronizace.
Označením metody modifikátorem synchronized ji automaticky synchronizujete na aktuální instanci (pro běžnou metodu) nebo na objektu Class (pro statickou metodu). To je nejjednodušší způsob zajištění vzájemného vyloučení, ale často je nadbytečný, pokud kritická sekce tvoří pouze malou část metody a zbytek kódu nevyžaduje synchronizaci.
Synchronized blok poskytuje přesnou kontrolu: určíte monitorovací objekt a synchronizujete pouze potřebnou část kódu, přičemž zbytek metody zůstává mimo zámek. To minimalizuje dobu držení monitoru a zvyšuje celkový výkon aplikace ve vícevláknovém prostředí, protože ostatní vlákna mohou paralelně spouštět nesouvisející kód bez čekání na uvolnění monitoru.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// kód mimo kritickou sekci - bez synchronizace
prepareData()
synchronized (lock) {
// pouze tento blok je chráněn
updateSharedState()
}
// pokračování bez zámku
cleanup()
}
}
| Kritérium | Synchronized metoda | Synchronized blok |
|---|---|---|
| Monitor | this (instance) nebo Class | libovolný objekt |
| Granularita | celá metoda | pouze potřebný kód |
| Čitelnost | vysoká | střední |
| Výkon | nižší u velké metody | vyšší u malé kritické sekce |
Při vývoji pro Android se synchronized široce používá k ochraně SharedPreferences, přístupu k databázi a komponent UI. Jeho použití na hlavním vlákně se však kategoricky nedoporučuje kvůli riziku zamrznutí rozhraní.
SharedPreferences v Android poskytuje základní bezpečnost vláken, ale při úpravě více vlákny přes Editor může být vyžadována externí synchronizace. Synchronized blok s odděleným objektem zámku zaručuje konzistenci změn.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
Hlavní omezení synchronized na Android — blokování vlákna. Na rozdíl od coroutin s Mutex, synchronized blokuje celé systémové vlákno. Na hlavním vlákně to způsobuje ANR. V moderním vývoji pro Android se doporučuje nahradit synchronized coroutinami (suspend Mutex) nebo atomickými typy (AtomicInteger).
Moderní Java a Kotlin nabízejí několik alternativ k synchronized, z nichž každá řeší stejné úkoly s menším počtem omezení nebo lepším výkonem.
Rozhraní Lock s implementacemi ReentrantLock a ReadWriteLock poskytuje časové limity, přerušitelné čekání a několik front Condition. Je flexibilnější než synchronized, ale vyžaduje explicitní uvolnění v finally, což zvyšuje riziko chyby při zapomenutém unlock.
Třídy AtomicInteger, AtomicLong, AtomicReference a další používají Lock-Free algoritmy založené na CAS (Compare-And-Swap). Jsou výrazně rychlejší než synchronized ve scénářích s mírnou konkurencí, protože neblokují vlákna, ale provádějí optimistické opakované pokusy a nevyžadují přepínání kontextu jádrem OS.
ThreadLocal poskytuje alternativní přístup: každá proměnná ThreadLocal je izolována v rámci jednoho vlákna a nevyžaduje synchronizaci pro čtení a zápis. To zcela eliminuje potřebu synchronized pro data, která by neměla být sdílena mezi vlákny. ThreadLocal je aktivně používán v rámcích (Spring, Hibernate) pro ukládání kontextu transakcí a relací.
V Kotlin projektech pro Android je alternativou k synchronized Mutex z kotlinx.coroutines. Neblokuje vlákno operačního systému, ale pozastaví coroutinu do uvolnění zámku — to umožňuje efektivní využití vláken z poolu a vyhnutí se ANR při dlouhém čekání na uvolnění prostředku.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
Výkon synchronized se v posledních verzích Java výrazně změnil. Dříve byl považován za „těžký„ mechanismus, ale moderní JVM odstranily většinu režie díky pokročilým optimalizacím JIT kompilátoru. Pojďme se podrobně podívat, jak virtuální stroj urychluje synchronizovaný kód za běhu.
JIT kompilátor JVM aplikuje několik optimalizací: biased locking eliminuje synchronizaci, pokud zámek vždy přebírá jedno vlákno; lock coarsening spojuje sousední synchronized bloky do jednoho; lock elimination odstraňuje synchronizaci, pokud je objekt přístupný pouze jednomu vláknu. Tyto optimalizace činí synchronized prakticky bezplatným při nízké konkurenci.
JVM určuje úroveň konkurence pro každý objekt: při nepřítomnosti konkurence se aktivuje biased locking, při objevení druhého vlákna přechází zámek do režimu lightweight se spin-čekáním a teprve při dlouhém čekání — do heavyweight se systémovým mutexem. Tato eskalace probíhá automaticky a vývojář nemusí ručně vybírat strategii.
V moderních benchmarkách (Java 17+) vykazuje synchronized výkon srovnatelný s ReentrantLock při nízké a střední konkurenci. Při vysoké konkurenci může mít Lock výhodu díky efektivnější frontě čekání s podporou timeoutů a přerušení. Pro vysoce zatížené systémy, kde je konkurence konstantní, poskytuje ReentrantLock v režimu fair předvídatelnější chování.
Atomické třídy (AtomicInteger, AtomicReference) zůstávají nejrychlejší pro jednoduché čítače a příznaky díky Lock-Free implementaci na CAS. Vůbec neblokují vlákna — při konfliktu se operace jednoduše opakuje ve smyčce. To poskytuje 3-5násobné zvýšení výkonu ve srovnání se synchronized u operací inkrementace čítače na 4-8 vláknech.
Často kladené otázky
Synchronized poskytuje jak vzájemné vyloučení, tak viditelnost. Volatile zaručuje pouze viditelnost změn — zápis do volatile proměnné je viditelný pro všechna vlákna, ale nebrání současné změně, tedy nechrání před závitovým prohledáváním.
Ano, deadlock je možný při vnořené synchronizaci s různým pořadím monitorů. Například jedno vlákno volá synchronized(a) { synchronized(b) }, a druhé — synchronized(b) { synchronized(a) }. Vyhněte se vnořeným synchronized blokům nebo stanovte jednotné pořadí monitorů.
Monitor je synchronizační mechanismus spojený s každým objektem Java. Zaručuje, že pouze jedno vlákno spouští synchronized kód na daném objektu. Monitor zahrnuje zámek, frontu čekání a sadu vláken čekajících na oznámení prostřednictvím wait/notify.
V moderních verzích Java (17+) synchronized nezaostává za Lockem ve výkonu díky JIT optimalizacím (biased locking, lock coarsening). Lock je preferován ne kvůli rychlosti, ale kvůli dalším možnostem: časové limity, přerušitelné čekání a několik Condition.
Statická synchronized metoda používá monitor objektu Class dané třídy, nikoli instance. To znamená, že synchronizace se vztahuje na všechny instance třídy. Nestatické a statické synchronized metody používají různé monitory a vzájemně se neblokují.
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é