Synchronized: що це, принцип дії та використання в Java

Автор: IT Sectr Опубліковано: 2026-03-19 Час читання: 8 хв

Synchronized — це вбудований механізм синхронізації в мові Java, що забезпечує ексклюзивний доступ до критичних секцій коду. За даними Oracle, 2024, модифікатор synchronized гарантує, що лише один потік може виконувати позначений метод або блок у конкретний момент часу. Цей механізм заснований на моніторах — фундаментальній концепції операційних систем, що забезпечує коректну роботу багатопотокових застосунків усіх рівнів складності.

Головне

  • Synchronized — ключове слово Java для потокобезпечного доступу до даних.
  • Монітор об'єкта — внутрішній механізм, на якому базується синхронізація.
  • Synchronized метод блокує весь метод на рівні екземпляра або класу.
  • Synchronized блок дозволяє синхронізувати лише частину коду.
  • Deadlock — одна з головних проблем при вкладеній синхронізації.

Що таке synchronized?

Synchronized — це ключове слово в Java, яке гарантує, що лише один потік одночасно виконує захищену ділянку коду, запобігаючи пошкодженню даних при паралельному доступі. Воно з'явилося в першій версії Java і залишається найпростішим способом забезпечення потокобезпечності для розробників будь-якого рівня підготовки.

Визначення та роль в Java

Модифікатор synchronized вирішує два завдання: взаємне виключення (mutual exclusion) та видимість змін (visibility). Коли потік виходить із synchronized-блоку, всі зміни гарантовано видимі іншим потокам, що входять у блок, синхронізований на тому ж об'єкті.

Synchronized можна застосувати до методу цілком або до довільного блоку коду із зазначенням об'єкта-монітора. В обох випадках JVM вставляє інструкції monitorenter та monitorexit на рівні байт-коду.

Передумови появи

У багатопотокових застосунках без синхронізації виникає стан гонки (race condition) — коли два потоки одночасно змінюють одні й ті ж дані, що призводить до непередбачуваних результатів. Synchronized став першим і основним інструментом Java для боротьби з цією проблемою, надаючи простий декларативний синтаксис, доступний будь-якому розробнику.

Як працює synchronized?

Механізм synchronized заснований на концепції монітора — високорівневого примітиву синхронізації, вбудованого в кожен об'єкт Java. Монітор зв'язується з об'єктом при першому використанні synchronized-блоку на ньому.

Монітор об'єкта

Кожен об'єкт в Java має асоційований монітор. Коли потік входить у synchronized-блок, він захоплює монітор об'єкта. Якщо монітор вже зайнятий іншим потоком, потік блокується до звільнення. У байт-коді цьому відповідає пара інструкцій monitorenter та monitorexit.

Стани блокування (biased locking)

JVM оптимізує synchronized через кілька рівнів: biased locking (зміщене блокування) для однопотокового доступу, lightweight locking (легке блокування) при слабкій конкуренції та heavyweight locking (важке блокування) при інтенсивній конкуренції з участю ОС. Ці рівні підвищують продуктивність без зміни коду.

java
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

Правило happens-before

Synchronized встановлює відношення happens-before: всі дії в потоці до виходу з synchronized-блоку видимі іншому потоку після входу в блок, синхронізований на тому ж об'єкті. Це гарантує не лише взаємне виключення, але й консистентність даних для всіх потоків.

Synchronized метод vs блок

Java пропонує два способи застосування synchronized: на рівні методу та на рівні блоку. Вибір між ними впливає на продуктивність та granularity синхронізації.

Synchronized метод

Позначивши метод модифікатором synchronized, ви автоматично синхронізуєте його на поточному екземплярі (для звичайного методу) або на об'єкті Class (для статичного методу). Це найпростіший спосіб забезпечення взаємного виключення, але він часто надлишковий, якщо критична секція становить лише малу частину методу, а решта коду не потребує синхронізації.

Synchronized блок

Synchronized блок дає точний контроль: ви вказуєте об'єкт-монітор і синхронізуєте лише необхідну ділянку коду, залишаючи решту методу поза блокуванням. Це мінімізує час утримання монітора та підвищує загальну продуктивність застосунку в багатопотоковому середовищі, оскільки інші потоки можуть паралельно виконувати непов'язаний код, не чекаючи звільнення монітора.

java
class DataProcessor {
    private final Object lock = new Object();

    public void process() {
        // код поза критичною секцією - без синхронізації
        prepareData()

        synchronized (lock) {
            // тільки цей блок захищений
            updateSharedState()
        }

        // продовження без блокування
        cleanup()
    }
}
КритерійSynchronized методSynchronized блок
Моніторthis (екземпляр) або Classбудь-який об'єкт
Granularityвесь методтільки потрібний код
Читабельністьвисокасередня
Продуктивністьнижча при великому методівища при малій критичній секції

Synchronized в Android

У розробці під Android synchronized широко застосовується для захисту SharedPreferences, доступу до БД та компонентів UI. Однак його використання на головному потоці категорично не рекомендується через ризик зависання інтерфейсу.

Використання з SharedPreferences

SharedPreferences в Android забезпечує базову потокобезпечність, але при редагуванні кількома потоками через Editor може знадобитися зовнішня синхронізація. Synchronized блок з окремим об'єктом блокування гарантує узгодженість змін.

kotlin
class PreferencesManager(private val prefs: SharedPreferences) {
    private val lock = Any()

    fun writeToken(token: String) {
        synchronized (lock) {
            prefs.edit()
                .putString("auth_token", token)
                .apply()
        }
    }
}

Обмеження в Android-застосунках

Головне обмеження synchronized на Android — блокування потоку. На відміну від корутин з Mutex, synchronized блокує системний потік цілком. На головному потоці це викликає ANR. У сучасній Android-розробці synchronized рекомендується замінювати на корутини (suspend Mutex) або атомарні типи (AtomicInteger).

Альтернативи synchronized

Сучасний Java та Kotlin пропонують кілька альтернатив synchronized, кожна з яких вирішує ті ж завдання з меншими обмеженнями або кращою продуктивністю.

Lock з java.util.concurrent

Інтерфейс Lock з реалізаціями ReentrantLock та ReadWriteLock надає таймаути, переривчасте очікування та кілька черг Condition. Він гнучкіший за synchronized, але вимагає явного звільнення в finally, що підвищує ризик помилки при забутому unlock.

Атомарні класи

Класи AtomicInteger, AtomicLong, AtomicReference та інші використовують Lock-Free алгоритми на основі CAS (Compare-And-Swap). Вони значно швидші за synchronized у сценаріях з помірною конкуренцією, оскільки не блокують потоки, а виконують оптимістичні повторні спроби без перемикання контексту ядром ОС.

ThreadLocal та безпека потоків

ThreadLocal надає альтернативний підхід: кожна змінна ThreadLocal ізольована в межах одного потоку і не потребує синхронізації для читання та запису. Це повністю усуває необхідність у synchronized для даних, які не повинні розділятися між потоками. ThreadLocal активно використовується в фреймворках (Spring, Hibernate) для зберігання контексту транзакцій та сесій.

Корутини та compose-підхід

У Kotlin-проєктах для Android альтернативою synchronized служить Mutex з kotlinx.coroutines. Він не блокує потік операційної системи, а призупиняє корутину до моменту звільнення блокування — це дозволяє ефективно використовувати потоки пулу та уникнути ANR при тривалому очікуванні звільнення ресурсу.

kotlin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val mutex = Mutex()
var counter = 0

suspend fun safeIncrement() {
    mutex.withLock {
        counter++
    }
}

Продуктивність synchronized

Продуктивність synchronized значно змінилася за останні версії Java. Раніше його вважали “важким” механізмом, але сучасні JVM усунули більшість накладних витрат завдяки передовим оптимізаціям JIT-компілятора. Розглянемо детально, як віртуальна машина прискорює синхронізований код у рантаймі.

Biased Locking та Lock Coarsening

JIT-компілятор JVM застосовує кілька оптимізацій: biased locking усуває синхронізацію, якщо блокування завжди захоплює один потік; lock coarsening об'єднує сусідні synchronized-блоки в один; lock elimination видаляє синхронізацію, якщо об'єкт доступний лише одному потоку. Ці оптимізації роблять synchronized практично безкоштовним при низькій конкуренції.

Вимірювання конкуренції та вибір оптимізації

JVM визначає рівень конкуренції для кожного об'єкта: при відсутності конкуренції вмикається biased locking, при появі другого потоку блокування переходить у lightweight-режим зі spin-очікуванням, і лише при тривалому очікуванні — у heavyweight з системним м'ютексом. Ця ескалація відбувається автоматично, і розробнику не потрібно вручну вибирати стратегію.

Порівняння з Lock та Atomic-класами

У сучасних бенчмарках (Java 17+) synchronized показує продуктивність, порівнянну з ReentrantLock при низькій та помірній конкуренції. При високій конкуренції Lock може мати перевагу завдяки більш ефективній черзі очікування з підтримкою timeouts та interrupt. Для високонавантажених систем, де конкуренція постійна, ReentrantLock з fair-режимом дає більш передбачувану поведінку.

Atomic-класи (AtomicInteger, AtomicReference) залишаються найшвидшими для простих лічильників та прапорців завдяки Lock-Free реалізації на CAS. Вони не блокують потоки взагалі — при конфлікті операція просто повторюється в циклі. Це дає приріст продуктивності в 3-5 разів порівняно з synchronized на операціях інкременту лічильника при 4-8 потоках.

Часто задавані питання

У чому різниця між synchronized та volatile?

Synchronized забезпечує і взаємне виключення, і видимість. Volatile гарантує лише видимість змін — запис у volatile-змінну видна всім потокам, але не запобігає одночасній зміні, тобто не захищає від стану гонки.

Чи може synchronized викликати deadlock?

Так, deadlock можливий при вкладеній синхронізації з різним порядком моніторів. Наприклад, один потік викликає synchronized(a) { synchronized(b) }, а інший — synchronized(b) { synchronized(a) }. Уникайте вкладених synchronized-блоків або фіксуйте єдиний порядок моніторів.

Що таке монітор в Java?

Монітор — це механізм синхронізації, пов'язаний з кожним об'єктом Java. Він гарантує, що лише один потік виконує synchronized-код на даному об'єкті. Монітор включає блокування, чергу очікування та пул потоків, що очікують на сповіщення через wait/notify.

Чи швидший Lock, ніж synchronized?

У сучасних версіях Java (17+) synchronized не поступається Lock за продуктивністю завдяки оптимізаціям JIT (biased locking, lock coarsening). Lock переважніше не через швидкість, а через додаткові можливості: таймаути, переривчасте очікування та кілька Condition.

Як synchronized працює зі статичними методами?

Статичний synchronized метод використовує монітор об'єкта Class даного класу, а не екземпляра. Це означає, що синхронізація поширюється на всі екземпляри класу. Нестатичні та статичні synchronized методи використовують різні монітори і не блокують один одного.

Підсумки

  • Synchronized — вбудований механізм синхронізації Java на основі моніторів.
  • Монітор об'єкта — внутрішня структура JVM, що забезпечує взаємне виключення.
  • Методи wait/notify використовуються лише всередині synchronized-блоків або методів.
  • Synchronized блок кращий за метод завдяки більш тонкій granularity синхронізації.
  • Happens-before гарантує видимість змін між потоками при синхронізації на одному об'єкті.
  • Deadlock — головний ризик при вкладеній синхронізації з різним порядком моніторів.
  • Альтернативи — Lock, Atomic-класи та корутини з suspending Mutex.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також