Synchronized — это встроенный механизм синхронизации в языке Java, обеспечивающий эксклюзивный доступ к критическим секциям кода. По данным Oracle, 2024, модификатор synchronized гарантирует, что только один поток может выполнять помеченный метод или блок в конкретный момент времени. Этот механизм основан на мониторах — фундаментальной концепции операционных систем, обеспечивающей корректную работу многопоточных приложений всех уровней сложности.
Главное
Synchronized — это ключевое слово в Java, которое гарантирует, что только один поток единовременно выполняет защищённый участок кода, предотвращая повреждение данных при параллельном доступе. Оно появилось в первой версии Java и остаётся самым простым способом обеспечения потокобезопасности для разработчиков любого уровня подготовки.
Модификатор synchronized решает две задачи: взаимное исключение (mutual exclusion) и видимость изменений (visibility). Когда поток выходит из synchronized-блока, все изменения гарантированно видны другим потокам, входящим в блок, синхронизированный на том же объекте.
Synchronised можно применить к методу целиком или к произвольному блоку кода с указанием объекта-монитора. В обоих случаях 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также