Как работает synchronized на уровне монитора
На уровне монитора ключевое слово synchronized обеспечивает взаимное исключение (mutual exclusion), гарантируя, что только один поток единовременно может исполнять критическую с...
🟢 Junior Level
Краткий ответ для собеседования (30 секунд)
На уровне монитора ключевое слово synchronized обеспечивает взаимное исключение (mutual exclusion), гарантируя, что только один поток единовременно может исполнять критическую секцию кода, привязанную к монитору целевого объекта.
Механика работы разделяется на два уровня:
- На уровне байткода:
synchronized-блок компилируется в явную пару инструкцийmonitorenter(захват монитора) иmonitorexit(освобождение монитора, генерируется как для нормального завершения, так и в блоке обработки любых исключений).synchronized-метод не содержит инструкцийmonitorenter/monitorexit, а помечается флагом доступаACC_SYNCHRONIZEDв таблице методов класса. Сама JVM автоматически выполняет захват монитора при входе в метод и освобождение при возврате.
- На уровне JVM (HotSpot): при входе поток пытается атомарно зафиксировать право владения в заголовке объекта (Mark Word). Если монитор свободен, поток входит без системных вызовов; если занят — поток либо кратковременно вращается в адаптивном цикле (Adaptive Spinning), либо паркуется операционной системой в состоянии
BLOCKED.
Объекты синхронизации в Java
Каждый тип вызова synchronized неявно или явно привязан к конкретному объекту-монитору:
| Конструкция | Объект монитора | Модификатор / Инструкция |
|---|---|---|
synchronized void method() |
Текущий экземпляр this |
Флаг ACC_SYNCHRONIZED |
static synchronized void method() |
Объект класса ClassName.class |
Флаг ACC_SYNCHRONIZED |
synchronized(lock) { ... } |
Явно указанный объект lock |
monitorenter / monitorexit |
synchronized(this) { ... } |
Текущий экземпляр this |
monitorenter / monitorexit |
Пример на Java 21
public class SynchronizedDemo {
private final Object customLock = new Object();
private int counter = 0;
// 1. Метод синхронизируется по монитору 'this' через ACC_SYNCHRONIZED
public synchronized void incrementInstance() {
counter++;
}
// 2. Блок синхронизируется по выделенному объекту customLock через monitorenter/exit
public void incrementBlock() {
synchronized (customLock) {
counter++;
}
}
// 3. Статический метод синхронизируется по объекту SynchronizedDemo.class
public static synchronized void globalReset() {
// Доступ к статическим ресурсам
}
}
🟡 Middle Level
Байткод: monitorenter / monitorexit против ACC_SYNCHRONIZED
Разберём различия в байткоде, генерируемом компилятором javac.
1. Синхронизированный блок (monitorenter / monitorexit)
public void execute(Object lock) {
synchronized (lock) {
doSomething();
}
}
Декомпиляция через javap -c -v:
public void execute(java.lang.Object);
Code:
0: aload_1 // Загрузка ссылки lock на стек
1: dup
2: astore_2 // Сохранение ссылки в локальную переменную 2
3: monitorenter // Атомарный захват монитора
4: aload_0
5: invokevirtual #7 // Вызов doSomething()
8: aload_2 // Загрузка ссылки lock
9: monitorexit // Нормальное освобождение монитора
10: goto 18
13: astore_3 // Перехват исключения (любой Throwable)
14: aload_2
15: monitorexit // ГАРАНТИРОВАННОЕ освобождение монитора при сбое!
16: aload_3
17: athrow // Повторный выброс пойманного исключения
18: return
Exception table:
from to target type
4 10 13 any // Охватывает тело критической секции
13 16 13 any // Охватывает обработчик исключения
2. Синхронизированный метод (ACC_SYNCHRONIZED)
public synchronized void executeMethod() {
doSomething();
}
Декомпиляция:
public synchronized void executeMethod();
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
0: aload_0
1: invokevirtual #7 // Method doSomething:()V
4: return
В байткоде метода нет инструкций monitorenter и monitorexit. При вызове метода исполнительный механизм JVM проверяет флаг ACC_SYNCHRONIZED:
- Для экземплярного метода JVM неявно запрашивает монитор объекта
this(переданного в нулевой локальной переменной). - Для статического метода запрашивается монитор объекта
java.lang.Class, представляющего данный класс. - При нормальном возврате (
return) или выбросе исключения JVM освобождает монитор автоматически на уровне фрейма вызова.
Реентерабельность (Reentrancy) на уровне JVM
Мониторы в Java строго реентерабельны: если поток уже владеет монитором, он может повторно войти в любой другой блок кода, защищённый этим же монитором, не вызывая взаимной блокировки (self-deadlock).
public synchronized void outer() {
inner(); // Тот же монитор 'this', deadlock НЕ происходит
}
public synchronized void inner() {
// Выполнение критического кода
}
Механизм поддержки реентерабельности:
- При Lightweight Lock (тонкая блокировка):
- Первый вход создает структуру
Lock Recordна стеке потока и копирует туда Displaced Mark Word. - Каждый последующий повторный вход тем же потоком создает на его стеке новый
Lock Record, но с нулевым Displaced Mark Word (null). Это служит маркером вложенного входа. - При каждом выходе
monitorexitстек снимает верхнийLock Record. Только когда снимается последнийLock Record(содержащий исходныйDisplaced Mark Word), монитор физически освобождается через CAS.
- Первый вход создает структуру
- При Heavyweight Lock (толстая блокировка):
- В системном объекте
ObjectMonitorинкрементируется поле_recursions. - При
monitorexitполе_recursionsдекрементируется. Монитор освобождается и становится доступным другим потокам только тогда, когда_recursionsстановится равным0.
- В системном объекте
🔴 Senior Level
Эволюция и эскалация блокировок (Lock Escalation) в HotSpot
Для достижения максимальной производительности JVM HotSpot оптимизирует работу synchronized, разделяя захват на Fast Path (на ассемблере) и Slow Path (системные вызовы ядра).
[ Unlocked Object ] (Mark Word: 01)
│
│ Поток выполняет monitorenter
v
[ Lightweight Locking (Thin Lock) ] (Mark Word: 00)
• CAS в Mark Word ссылки на Lock Record стека
• Затраты: единицы наносекунд, без обращения к ядру ОС
│
│ Возникновение конкуренции (CAS сбоит)
v
[ Adaptive Spinning ]
• Циклическое вращение на CPU в ожидании освобождения
│
│ Превышен лимит спиннинга
v
[ Lock Inflation -> Heavyweight Lock (Fat Lock) ] (Mark Word: 10)
• Аллокация C++ объекта ObjectMonitor в нативной памяти
• Потоки выстраиваются в очереди _cxq и _EntryList
• Системный вызов парковки ОС (futex в Linux, WaitForSingleObject в Windows)
• Состояние потока: BLOCKED. Затраты на context switch: ~1000-5000 ns
[!IMPORTANT] Удаление Biased Locking: Начиная с Java 15 Biased Locking был выключен по умолчанию, а в Java 21 полностью удален (JEP 416). Причины: огромная сложность кодовой базы HotSpot, дорогие Safepoint-паузы для отзыва блокировок (Revocation STW) и малый выигрыш на современных многоядерных архитектурах с развитыми атомарными инструкциями.
Семантика Модели Памяти Java (JMM): Барьеры памяти
Ключевое слово synchronized накладывает строгие требования на компилятор и процессор в рамках JSR-133:
- Вход в монитор (
monitorenter):- Обладает семантикой Acquire.
- Ни одна операция чтения или записи, находящаяся внутри синхронизированного блока, не может быть вынесена процессором или JIT-компилятором перед входом в монитор.
- Локальные процессорные кеши потока инвалидируются, заставляя читать актуальные значения переменных из общей памяти.
- Выход из монитора (
monitorexit):- Обладает семантикой Release.
- Ни одна операция записи, находящаяся внутри синхронизированного блока, не может быть отложена и выполнена после освобождения монитора.
- Все изменения, сделанные потоком внутри критической секции, сбрасываются из Store Buffer / L1 кеша в общую видимость (L3/RAM).
- Отношение Happens-Before:
Освобождение монитора (
monitorexit) на объекте happens-before каждому последующему захвату (monitorenter) того же самого монитора.
Внутренние очереди HotSpot: разница между _cxq и _EntryList
В структуре ObjectMonitor конкурирующие потоки организуются в две независимые структуры данных:
Новые конкурирующие потоки
│
│ CAS push (Lock-Free LIFO Stack)
v
[ _cxq ] (Contention Queue)
│
│ Поток-владелец при выходе из monitorexit переносит элементы
v
[ _EntryList ] (Очередь готовых к захвату потоков)
│
│ Пробуждение следующего потока (_succ)
v
[ Поток-Владелец (_owner) ]
_cxq(Contention Queue): односвязный неблокирующий LIFO-стек. Все потоки, которым не удалось захватить монитор через CAS, атомарно добавляются в голову_cxqбез глобальных блокировок._EntryList: двусвязный список, используемый владельцем монитора при выборе преемника. В зависимости от внутренней политики HotSpot (Knob_QMode), выходящий поток переносит узлы из_cxqв_EntryListв порядке FIFO или LIFO._succ(Successor): оптимизация «эстафетной палочки». Поток-владелец перед выходом назначает один поток из очереди преемником (_succ) и разблокирует его. Этот поток пробуждается и сразу пытается захватить монитор, избегая массового пробуждения («thundering herd problem»).
JIT-Оптимизации: Lock Elision и Lock Coarsening
Компилятор C2 производит агрессивную оптимизацию мониторов:
1. Lock Elision (Устранение блокировок на основе Escape Analysis)
Если компилятор доказывает, что объект блокировки не выходит за пределы текущего потока (NoEscape), операции monitorenter и monitorexit полностью вырезаются из машинного кода:
public String formatMessage(String message) {
// StringBuffer синхронизирован, но ссылка sb локальна и не "убегает"
StringBuffer sb = new StringBuffer();
sb.append("Prefix: ").append(message);
return sb.toString(); // JIT C2 полностью устраняет все блокировки!
}
2. Lock Coarsening (Укрупнение блокировок)
Если в коде следуют подряд несколько синхронизаций по одному и тому же монитору, JIT объединяет их в один общий блок, сокращая оверхед на захват и сброс кешей:
// Исходный байткод:
synchronized(lock) { a(); }
synchronized(lock) { b(); }
synchronized(lock) { c(); }
// Оптимизированный JIT-код:
synchronized(lock) {
a();
b();
c();
}
🎯 Шпаргалка для интервью
1. 30-секундный эссенс (Elevator Pitch)
На уровне монитора synchronized обеспечивает взаимное исключение и видимость изменений (JMM Happens-Before). synchronized-блок компилируется в байткод-инструкции monitorenter/monitorexit со скрытым блоком try-finally для гарантированного освобождения при исключениях, а synchronized-метод управляется флагом ACC_SYNCHRONIZED. В JVM HotSpot блокировка эскалируется: от атомарного CAS на стеке (Lightweight Lock) до системного объекта ObjectMonitor (Heavyweight Lock) с парковкой потоков в состоянии BLOCKED через системные вызовы ОС. Устаревший Biased Locking удалён в Java 21.
2. 4 каверзных вопроса с глубокими ответами
Вопрос 1: В чём ключевое отличие в обработке исключений между synchronized-блоком и synchronized-методом?
Ответ:
- В
synchronized-блоке компиляторjavacявно генерирует таблицу исключений (Exception table) с таргетомany, охватывающую диапазон от первой инструкции внутри блока до инструкции нормальногоmonitorexit. В обработчике дублируется инструкцияmonitorexit, после чего следуетathrow. - В
synchronized-методе таблица исключений для монитора не генерируется! Захват и освобождение монитора выполняет сам исполнительный механизм вызова методов JVM при наличии флагаACC_SYNCHRONIZED. Если из метода вылетает необработанное исключение, монитор освобождается JVM во время раскрутки стека (stack unwinding) до возврата в вызывающий фрейм.
Вопрос 2: Почему synchronized(new Integer(1)) или synchronized("admin") — это критическая архитектурная ошибка?
Ответ:
- Интернирование строк (
String pool): строковые литералы интернируются JVM. Если разные компоненты приложения или сторонние библиотеки напишутsynchronized("admin"), они неожиданно начнут блокироваться на одном и том же разделяемом глобальном объектеString, порождая непредсказуемые паузы и взаимные блокировки (Deadlock). - Кеширование обёрток примитивов (Integer Cache): значения
Integer.valueOf()от -128 до 127 кешируются. Блокировка по закешированному числу эквивалентна глобальной блокировке. - Value-based Classes (Project Valhalla): в современных Java классы-обёртки (
Integer,Longи др.) помечены как@ValueBased. Синхронизация по ним объявлена deprecated for removal и выбрасывает предупреждения, а в будущих версиях Java приведёт к выбросуIdentityException. Блокироваться следует только на приватных экземплярахnew Object().
Вопрос 3: Что произойдёт, если поток будет прерван (Thread.interrupt()), находясь в состоянии ожидания захвата монитора (BLOCKED)?
Ответ:
Поток не отреагирует на прерывание и не выбросит InterruptedException.
Ожидание входа в synchronized (на уровне monitorenter) является непрерываемым (uninterruptible). Поток продолжит находиться в очереди _cxq/_EntryList в состоянии BLOCKED до тех пор, пока монитор не освободится. Флаг прерывания (isInterrupted() == true) сохранится, но поток сможет проверить его только после того, как физически войдёт в критическую секцию.
Если требуется прерываемое ожидание блокировки, необходимо использовать Lock.lockInterruptibly() или Lock.tryLock(timeout) из пакета java.util.concurrent.locks.
Вопрос 4: Почему synchronized(this) практически никогда не подвергается оптимизации Lock Elision со стороны JIT-компилятора?
Ответ:
Lock Elision возможен только в том случае, если JIT на основе анализа побега (Escape Analysis) докажет статус объекта как NoEscape (объект создан внутри метода и никуда не передаётся). Ссылка this указывает на текущий экземпляр, который был создан в вызывающем коде и доступен внешним потокам (статус ArgEscape или GlobalEscape). Поскольку компилятор не может гарантировать, что другой поток параллельно не обратится к тому же экземпляру, JIT обязан сохранить инструкции синхронизации. Lock Elision работает только для локально созданных объектов-блокировок.
3. Типичные ошибки и Red Flags
❌ “Synchronized-метод быстрее synchronized-блока, потому что в нём меньше байткода.”
Правильно: Разница в производительности между ACC_SYNCHRONIZED и monitorenter/monitorexit на современных JIT-компиляторах исчезающе мала или отсутствует. Синхронизированный блок предпочтительнее с архитектурной точки зрения, так как позволяет сузить критическую секцию до минимума и инкапсулировать объект блокировки через private final Object lock.
❌ “Поток, ожидающий входа в synchronized, находится в состоянии WAITING.”
Правильно: Поток, конкурирующий за монитор при входе в synchronized, находится в состоянии Thread.State.BLOCKED. В состояние WAITING поток переходит только после явного вызова wait().
❌ “Если внутри synchronized-блока вызвать sleep(), монитор освободится.”
Правильно: Метод Thread.sleep() никогда не освобождает удерживаемые мониторы. Поток просто засыпает (переходит в TIMED_WAITING), продолжая держать монитор и блокируя всех остальных конкурентов. Освобождает монитор только метод Object.wait().
❌ “При выходе из synchronized по исключению монитор остаётся заблокированным.”
Правильно: Байткод гарантирует вызов monitorexit через неявный блок перехвата исключений (Exception table), предотвращая зависание монитора.