Synchronized је уграђени механизам синхронизације у језику Java, који обезбеђује ексклузивни приступ критичним секцијама кода. Према Oracle, 2024, модификатор synchronized гарантује да само једна нит може извршавати означену методу или блок у одређеном тренутку. Овај механизам се заснива на мониторима — фундаменталном концепту оперативних система, који обезбеђује коректно функционисање вишенитних апликација свих нивоа сложености.
Главно
Synchronized је кључна реч у Java која гарантује да само једна нит истовремено извршава заштићени део кода, спречавајући оштећење података при паралелном приступу. Појавила се у првој верзији Java и остаје најједноставнији начин обезбеђивања безбедности нити за програмере свих нивоа.
Модификатор synchronized решава два задатка: међусобно искључивање (mutual exclusion) и видљивост промена (visibility). Када нит напусти synchronized блок, све промене су гарантовано видљиве другим нитима које улазе у блок синхронизован на истом објекту.
Synchronized се може применити на целу методу или на произвољни блок кода са навођењем објекта-монитора. У оба случаја JVM убацује инструкције monitorenter и monitorexit на нивоу бајт-кода.
У вишенитним апликацијама без синхронизације настаје тркачко стање (race condition) — када две нити истовремено мењају исте податке, доводећи до непредвидивих резултата. Synchronized је постао први и главни алат Java за борбу против овог проблема, пружајући једноставну декларативну синтаксу доступну сваком програмеру.
Механизам synchronized се заснива на концепту монитора — високонивоског примитива синхронизације уграђеног у сваки Java објекат. Монитор се повезује са објектом при првом коришћењу synchronized блока на њему.
Сваки објекат у Java има повезани монитор. Када нит уђе у synchronized блок, она преузима монитор објекта. Ако је монитор већ заузет другом нити, нит се блокира до ослобађања. У бајт-коду овоме одговара пар инструкција monitorenter и monitorexit.
JVM оптимизује synchronized кроз неколико нивоа: biased locking (пристрасно закључавање) за једнонитни приступ, lightweight locking (лако закључавање) при слабој конкуренцији и heavyweight locking (тешко закључавање) при интензивној конкуренцији са учешћем ОС. Ови нивои повећавају перформансе без промене кода.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized успоставља релацију happens-before: све радње у нити пре изласка из synchronized блока видљиве су другој нити након уласка у блок синхронизован на истом објекту. Ово гарантује не само међусобно искључивање, већ и конзистентност података за све нити.
Java нуди два начина примене synchronized: на нивоу методе и на нивоу блока. Избор између њих утиче на перформансе и грануларност синхронизације.
Обележавањем методе модификатором synchronized, аутоматски је синхронизујете на текућој инстанци (за обичну методу) или на објекту Class (за статичку методу). Ово је најједноставнији начин обезбеђивања међусобног искључивања, али је често претерано ако критична секција чини само мали део методе, а остатак кода не захтева синхронизацију.
Synchronized блок даје прецизну контролу: наводите објекат-монитор и синхронизујете само потребан део кода, остављајући остатак методе ван закључавања. Ово минимизира време држања монитора и повећава укупне перформансе апликације у вишенитном окружењу, јер друге нити могу паралелно извршавати неповезани код без чекања на ослобађање монитора.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// код ван критичне секције - без синхронизације
prepareData()
synchronized (lock) {
// само овај блок је заштићен
updateSharedState()
}
// наставак без закључавања
cleanup()
}
}
| Критеријум | Synchronized метода | Synchronized блок |
|---|---|---|
| Монитор | this (инстанца) или Class | било који објекат |
| Грануларност | цела метода | само потребан код |
| Читљивост | висока | средња |
| Перформансе | ниже код велике методе | више код мале критичне секције |
У развоју за Android, synchronized се широко користи за заштиту SharedPreferences, приступа бази података и UI компоненти. Међутим, његова употреба на главној нити се категорички не препоручује због ризика од замрзавања интерфејса.
SharedPreferences у Android обезбеђује основну безбедност нити, али при уређивању од стране више нити преко Editor-а може бити потребна спољашња синхронизација. Synchronized блок са засебним објектом закључавања гарантује конзистентност промена.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
Главно ограничење synchronized на Android — блокирање нити. За разлику од корутина са Mutex, synchronized блокира целу системску нит. На главној нити ово изазива ANR. У модерном Android развоју препоручује се замена synchronized корутинама (suspend Mutex) или атомским типовима (AtomicInteger).
Модерни Java и Kotlin нуде неколико алтернатива за synchronized, од којих свака решава исте задатке са мање ограничења или бољим перформансама.
Интерфејс Lock са имплементацијама ReentrantLock и ReadWriteLock пружа тајмауте, прекидиво чекање и више Condition редова. Флексибилнији је од synchronized, али захтева експлицитно ослобађање у finally, што повећава ризик од грешке при заборављеном unlock.
Класе AtomicInteger, AtomicLong, AtomicReference и друге користе Lock-Free алгоритме засноване на CAS (Compare-And-Swap). Оне су значајно брже од synchronized у сценаријима са умереном конкуренцијом, јер не блокирају нити, већ извршавају оптимистичке поновне покушаје и не захтевају пребацивање контекста од стране језгра ОС.
ThreadLocal пружа алтернативни приступ: свака ThreadLocal променљива је изолована у оквиру једне нити и не захтева синхронизацију за читање и писање. Ово потпуно елиминише потребу за synchronized за податке који не треба да се деле између нити. ThreadLocal се активно користи у фрејмворцима (Spring, Hibernate) за чување контекста транзакција и сесија.
У Kotlin пројектима за Android, алтернатива synchronized-у је Mutex из kotlinx.coroutines. Он не блокира нит оперативног система, већ суспендује корутину до тренутка ослобађања закључавања — ово омогућава ефикасно коришћење нити из pool-а и избегавање ANR-а при дугом чекању на ослобађање ресурса.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
Перформансе synchronized су се значајно промениле у последњим верзијама Java. Раније се сматрао „тешким„ механизмом, али модерне JVM су елиминисале већину додатних оптерећења захваљујући напредним оптимизацијама JIT компајлера. Размотримо детаљно како виртуелна машина убрзава синхронизовани код у рантајму.
JIT компајлер JVM примењује неколико оптимизација: biased locking елиминише синхронизацију ако закључавање увек преузима једна нит; lock coarsening обједињује суседне synchronized блокове у један; lock elimination уклања синхронизацију ако је објекат доступан само једној нити. Ове оптимизације чине synchronized практично бесплатним при ниској конкуренцији.
JVM одређује ниво конкуренције за сваки објекат: при одсуству конкуренције укључује се biased locking, при појави друге нити закључавање прелази у lightweight режим са spin-чекањем, и тек при дугом чекању — у heavyweight са системским мутексом. Ова ескалација се дешава аутоматски и програмер не мора ручно да бира стратегију.
У модерним бенчмарковима (Java 17+) synchronized показује перформансе упоредиве са ReentrantLock при ниској и умереној конкуренцији. При високој конкуренцији Lock може имати предност захваљујући ефикаснијем реду чекања са подршком за тајмауте и прекиде. За високооптерећене системе где је конкуренција константна, ReentrantLock у fair режиму даје предвидљивије понашање.
Атомске класе (AtomicInteger, AtomicReference) остају најбрже за једноставне бројаче и заставице захваљујући Lock-Free имплементацији на CAS. Оне уопште не блокирају нити — при конфликту операција се једноставно понавља у петљи. Ово даје повећање перформанси од 3-5 пута у поређењу са synchronized на операцијама инкремента бројача при 4-8 нити.
Често постављана питања
Synchronized обезбеђује и међусобно искључивање и видљивост. Volatile гарантује само видљивост промена — упис у volatile променљиву видљив је свим нитима, али не спречава истовремену измену, односно не штити од тркачког стања.
Да, deadlock је могућ код угњеждене синхронизације са различитим редоследом монитора. На пример, једна нит позива synchronized(a) { synchronized(b) }, а друга — synchronized(b) { synchronized(a) }. Избегавајте угњеждене synchronized блокове или фиксирајте јединствени редослед монитора.
Монитор је механизам синхронизације повезан са сваким Java објектом. Он гарантује да само једна нит извршава synchronized код на датом објекту. Монитор укључује закључавање, ред чекања и скуп нити које чекају обавештење путем wait/notify.
У модерним верзијама Java (17+) synchronized не заостаје за Lock-ом у перформансама захваљујући JIT оптимизацијама (biased locking, lock coarsening). Lock се преферира не због брзине, већ због додатних могућности: тајмаути, прекидиво чекање и више Condition.
Статичка synchronized метода користи монитор објекта Class дате класе, а не инстанце. То значи да се синхронизација простире на све инстанце класе. Нестатичке и статичке synchronized методе користе различите мониторе и не блокирају једна другу.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође