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