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: на рівні методу та на рівні блоку. Вибір між ними впливає на продуктивність та granularity синхронізації.
Позначивши метод модифікатором synchronized, ви автоматично синхронізуєте його на поточному екземплярі (для звичайного методу) або на об'єкті Class (для статичного методу). Це найпростіший спосіб забезпечення взаємного виключення, але він часто надлишковий, якщо критична секція становить лише малу частину методу, а решта коду не потребує синхронізації.
Synchronized блок дає точний контроль: ви вказуєте об'єкт-монітор і синхронізуєте лише необхідну ділянку коду, залишаючи решту методу поза блокуванням. Це мінімізує час утримання монітора та підвищує загальну продуктивність застосунку в багатопотоковому середовищі, оскільки інші потоки можуть паралельно виконувати непов'язаний код, не чекаючи звільнення монітора.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// код поза критичною секцією - без синхронізації
prepareData()
synchronized (lock) {
// тільки цей блок захищений
updateSharedState()
}
// продовження без блокування
cleanup()
}
}
| Критерій | Synchronized метод | Synchronized блок |
|---|---|---|
| Монітор | this (екземпляр) або Class | будь-який об'єкт |
| Granularity | весь метод | тільки потрібний код |
| Читабельність | висока | середня |
| Продуктивність | нижча при великому методі | вища при малій критичній секції |
У розробці під 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 може мати перевагу завдяки більш ефективній черзі очікування з підтримкою timeouts та interrupt. Для високонавантажених систем, де конкуренція постійна, ReentrantLock з fair-режимом дає більш передбачувану поведінку.
Atomic-класи (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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також