Что такое монитор (monitor) в Java
Каждый Java-объект «из коробки» обладает собственным встроенным монитором (intrinsic lock или monitor lock). Монитор решает две ключевые задачи:
🟢 Junior Level
Краткий ответ для собеседования (30 секунд)
Монитор (Monitor) в Java — это высокоуровневый механизм синхронизации и взаимного исключения (mutual exclusion), предложенный Ч. Хоаром и П. Бринчем Хансеном и встроенный в платформу Java на уровне языка и виртуальной машины JVM.
Каждый Java-объект «из коробки» обладает собственным встроенным монитором (intrinsic lock или monitor lock). Монитор решает две ключевые задачи:
- Взаимное исключение (Mutual Exclusion): гарантирует, что в любой момент времени только один поток может выполнять критическую секцию кода, защищённую данным монитором (ключевое слово
synchronized). - Координация и сигнализация (Cooperation): позволяет потокам координировать совместную работу, добровольно приостанавливая выполнение до наступления определённого условия с помощью методов
wait(),notify()иnotifyAll().
Архитектурные компоненты монитора
Концептуально монитор в Java состоит из трёх составляющих:
- Lock (Замок / Владелец): ссылка на поток, который в данный момент успешно захватил монитор и выполняет критическую секцию.
- Entry Set (Очередь входа /
EntryList): множество потоков, которые пытаются войти вsynchronized-блок, но монитор уже занят другим потоком. Эти потоки переводятся в состояниеBLOCKED. - Wait Set (Множество ожидания /
WaitSet): множество потоков, которые вызвали методwait()внутриsynchronized-блока. Они добровольно освободили монитор и ждут сигнала (notify()/notifyAll()). Их состояние —WAITINGилиTIMED_WAITING.
+------------------------------------+
| Поток вызывает monitorenter |
+-----------------+------------------+
|
v
[ Entry Set ] <--------------------+
(Состояние BLOCKED) |
| | notify() /
Монитор свободен | захват | notifyAll()
v |
+-----------------+ |
| Владелец (Owner)| |
| (RUNNABLE) | |
+--------+--------+ |
| |
lock.wait() | |
(освобождает монитор) v |
[ Wait Set ] ---------------------+
(WAITING / TIMED_WAITING)
Базовый пример на Java 21
public class BoundedBlockingQueue<T> {
private final Object lock = new Object(); // Выделенный объект для монитора
private final int capacity;
private final java.util.Queue<T> queue = new java.util.LinkedList<>();
public BoundedBlockingQueue(int capacity) {
this.capacity = capacity;
}
// Поток захватывает монитор объекта 'lock'
public void put(T item) throws InterruptedException {
synchronized (lock) {
// Ожидание в цикле while для защиты от ложных пробуждений (spurious wakeups)
while (queue.size() == capacity) {
lock.wait(); // Освобождает монитор и переходит в WaitSet (WAITING)
}
queue.add(item);
lock.notifyAll(); // Будит потоки из WaitSet, переводя их в EntrySet
} // Монитор автоматически освобождается при выходе из блока
}
public T take() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait();
}
T item = queue.poll();
lock.notifyAll();
return item;
}
}
}
🟡 Middle Level
Внутреннее устройство: Object Header и Mark Word
В HotSpot JVM каждый Java-объект в куче (Heap) имеет заголовок (Object Header), состоящий из:
- Klass Word: указатель на метаданные класса в Metaspace (4 байта при включенном
UseCompressedClassPointers, иначе 8 байт). - Mark Word: 64-битное (на 64-битных архитектурах) машинное слово, отражающее текущее состояние объекта: хеш-код, возраст сборки мусора (GC age) и состояние блокировки.
Состояния Mark Word (HotSpot 64-bit, Java 21)
Младшие 2 бита Mark Word определяют состояние монитора:
| Состояние блокировки | Бит смещения (Bias) | Биты состояния (Tag) | Описание содержимого Mark Word |
|---|---|---|---|
| Unlocked (Без блокировки) | 0 |
01 |
Hashcode (31 бит), GC Age (4 бита), unused |
| Lightweight Locked (Тонкий лок) | 0 |
00 |
Указатель на Lock Record на стеке потока |
| Heavyweight Locked (Толстый лок) | 0 |
10 |
Указатель на системный C++ объект ObjectMonitor |
| Marked for GC | 0 |
11 |
Используется сборщиком мусора во время маркировки |
[!NOTE] Начиная с Java 15 механизм Biased Locking (смещённая блокировка) был отключён по умолчанию (JEP 374), а в Java 21 полностью удалён из JVM HotSpot (JEP 416). Современная эскалация блокировок в Java 21 начинается сразу с Lightweight Locking.
Анатомия C++ структуры ObjectMonitor
Когда монитор эскалируется в тяжёлую системную блокировку (Heavyweight Lock), JVM выделяет системную C++ структуру ObjectMonitor.
Упрощённое представление из исходного кода OpenJDK (src/hotspot/share/runtime/objectMonitor.hpp):
class ObjectMonitor {
void* _header; // Исходный Mark Word объекта (displaced mark word)
void* _object; // Указатель на сам Java-объект
Thread* volatile _owner; // Указатель на поток-владелец
volatile intx _recursions; // Счётчик реентерабельных входов (reentrant depth)
// Очереди ожидания потоков:
ObjectWaiter* volatile _cxq; // Contention Queue (LIFO lock-free стек конкурирующих потоков)
ObjectWaiter* volatile _EntryList; // Очередь потоков, готовых бороться за владение
ObjectWaiter* volatile _WaitSet; // Односвязный кольцевой список потоков, вызвавших wait()
volatile intx _WaitSetLock; // Защита структуры WaitSet
Thread* volatile _succ; // Наследник (Successor) — поток, назначенный стать следующим владельцем
};
Роли очередей _cxq и _EntryList
_cxq(Contention Queue): односвязный неблокирующий стек (LIFO), куда вновь прибывшие потоки атомарно добавляются с помощью инструкции CAS (Compare-And-Swap), когда монитор занят._EntryList: очередь потоков, сформированная потоком-владельцем при выходе. При освобождении монитора поток-владелец по определённой политике переносит потоки из_cxqв_EntryListи пробуждает поток_succ.
Байткод: инструкции monitorenter и monitorexit
Синхронизированный блок на уровне байткода явно компилируется в инструкции monitorenter и monitorexit.
Рассмотрим декомпиляцию через javap -c:
public void execute(Object lock) {
synchronized (lock) {
doWork();
}
}
public void execute(java.lang.Object);
Code:
0: aload_1 // Загрузка ссылки lock на стек операндов
1: dup
2: astore_2 // Сохранение ссылки во временную переменную
3: monitorenter // Захват монитора объекта
4: aload_0
5: invokevirtual #7 // Method doWork:()V
8: aload_2
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
[!IMPORTANT] Компилятор
javacавтоматически оборачивает телоsynchronized-блока в неявную секциюtry-finallyчерез таблицу исключений (Exception table). Это гарантирует, что даже при возникновении любого необработанного исключения (RuntimeException,Error), инструкцияmonitorexitвыполнится и монитор не останется заблокированным навсегда.
🔴 Senior Level
Эволюция и эскалация блокировок (Lock Escalation / Inflation)
В современной JVM (Java 21) переход состояний блокировки строго однонаправлен при конкуренции:
[ Unlocked (01) ]
│
│ Первый поток входит в synchronized
v
[ Lightweight Lock (00) ] <--- Lock Record на стеке потока (CAS в Mark Word)
│
│ Конкуренция (CAS фейлится) + Адаптивный спиннинг исчерпан
v
[ Heavyweight Lock (10) ] <--- Выделение ObjectMonitor, парковка потока в ОС (futex/mutex)
1. Lightweight Locking (Тонкая блокировка)
- Когда поток входит в
synchronized, на его стеке выделяется структураBasicObjectLock(Lock Record). - Исходный незаблокированный
Mark Wordобъекта копируется вLock Record(называется Displaced Mark Word). - JVM выполняет атомарную процессорную инструкцию
lock cmpxchg(CAS), пытаясь записать указатель наLock RecordвMark Wordобъекта. - Если CAS успешен: поток стал владельцем тонкого лока без обращения к системным вызовам ядра ОС. Затраты составляют всего несколько тактов процессора!
- При реентерабельном входе (повторный вход тем же потоком) в новый
Lock Recordзаписываетсяnullвместо Displaced Mark Word, что служит признаком вложенного входа.
2. Inflation до Heavyweight Lock (Раздувание монитора)
Если второй поток пытается захватить монитор, пока первый удерживает Lightweight Lock:
- CAS завершается неудачей.
- Поток выполняет Adaptive Spinning (адаптивное вращение): крутится в коротком цикле процессора, рассчитывая, что удерживающий поток быстро освободит лок.
- Если за время спиннинга лок не освободился, происходит Lock Inflation:
- JVM выделяет структуру
ObjectMonitor. - В поле
_headerструктурыObjectMonitorпомещается Displaced Mark Word. - В
Mark Wordобъекта атомарно прописывается указатель наObjectMonitorи устанавливаются биты10. - Конкурирующий поток помещает себя в стек
_cxqи засыпает (паркуется) через системный вызов ядра ОС (futexв Linux илиWaitForSingleObjectв Windows). - Поток переходит в статус
Thread.State.BLOCKED.
- JVM выделяет структуру
Дефляция мониторов (Async Monitor Deflation)
Исторически тяжёлый ObjectMonitor мог быть сдут (deflated) обратно до состояния Unlocked только во время полной паузы Stop-The-World (STW) сборщика мусора. Это вызывало длительные задержки пауз GC при большом количестве раздутых мониторов.
Начиная с Java 15/17+ в HotSpot внедрён механизм Asynchronous Monitor Deflation (JEP 350 / JEP 374):
- Фоновый сервисный поток JVM (
MonitorDeflationThread) периодически асинхронно сканирует неиспользуемыеObjectMonitorбез остановки рабочих потоков приложения (in safepoint-free manner). - Если у монитора
_owner == null,_cxq == null,_EntryList == nullи_WaitSet == null, JVM восстанавливает Displaced Mark Word обратно в объект и освобождает памятьObjectMonitor.
Влияние Object.hashCode() на мониторы
Один из самых неочевидных механизмов JVM:
- Если объект находится в состоянии Lightweight Lock, в его
Mark Wordхранится указатель на стек. Настоящий хеш-код объекта хранится вLock Record. - Если на объекте, удерживаемом в Thin Lock, вызывается метод
System.identityHashCode(obj)или дефолтныйobj.hashCode(), JVM не может сохранить 31-битный identity hashcode вLock Recordбез риска рассинхронизации. - Следствие: вызов identity hashcode на заблокированном объекте немедленно форсирует инфляцию тонкой блокировки до Heavyweight Lock (
ObjectMonitor), так как только вObjectMonitor._headerгарантированно есть постоянное место для хранения вычисленного identity hashcode на всё время жизни объекта!
Оптимизации JIT-компилятора C2
JIT-компилятор анализирует использование мониторов и применяет мощные оптимизации:
- Lock Elision (Удаление блокировки):
На основе Escape Analysis (анализа побега), если JIT определяет, что объект, по которому происходит синхронизация, не выходит за пределы текущего метода/потока (thread-local), вызовы
monitorenterиmonitorexitполностью вырезаются из машинного кода:public String concatBuffers(String a, String b) { // StringBuffer потокобезопасен и использует synchronized // JIT видит, что sb не убегает из метода, и удаляет все блокировки! StringBuffer sb = new StringBuffer(); sb.append(a).append(b); return sb.toString(); } - Lock Coarsening (Укрупнение блокировки):
Если компилятор видит цепочку последовательных синхронизаций по одному и тому же монитору, он объединяет их в один большой блок, устраняя накладные расходы на постоянный вход и выход:
// До оптимизации: for (int i = 0; i < 1000; i++) { synchronized (lock) { process(i); } } // После Lock Coarsening: synchronized (lock) { for (int i = 0; i < 1000; i++) { process(i); } }
🎯 Шпаргалка для интервью
1. 30-секундный эссенс (Elevator Pitch)
Монитор в Java — это встроенная языковая абстракция взаимного исключения и координации потоков. Каждый объект Java имеет встроенный монитор (intrinsic lock). На уровне HotSpot JVM синхронизация оптимизирована через эскалацию: при отсутствии конкуренции работает легковесный Lightweight Lock через CAS в Mark Word на стеке; при возникновении конкуренции происходит Lock Inflation в системный Heavyweight Lock (ObjectMonitor), переводящий потоки в состояние BLOCKED через системные вызовы ядра ОС. Устаревший Biased Locking полностью удалён в Java 21.
2. 4 каверзных вопроса с глубокими ответами
Вопрос 1: Почему методы wait(), notify() и notifyAll() объявлены в java.lang.Object, а не в java.lang.Thread?
Ответ:
- Принцип инкапсуляции и адресации: потоки ждут наступления определённого состояния данных (условия), а не другого потока. Данные инкапсулированы внутри Java-объектов.
- Связь с монитором:
wait()иnotify()работают непосредственно с очередьюWaitSetконкретного монитора объекта. Если бы метод принадлежал классуThread, было бы невозможно определить, какой именно монитор поток должен освободить при уходе в ожидание, особенно если поток удерживает несколько вложенных мониторов одновременно. - Метод
wait()освобождает только монитор того объекта, на котором он вызван, сохраняя блокировки по всем остальным вложенным мониторам!
Вопрос 2: Что происходит с монитором, если внутри synchronized-блока выбрасывается необработанный OutOfMemoryError или поток аварийно завершается?
Ответ:
Монитор гарантированно освобождается. Компилятор javac всегда генерирует неявную таблицу обработки исключений (Exception table) с типом перехвата any, оборачивающую весь критический участок. Любой Throwable приводит к исполнению инструкции monitorexit перед вылетом исключения наружу. Никакого зависания монитора в заблокированном состоянии не произойдет. (Исключение — жесткое падение всей JVM по SIGKILL или зависание в нативном JNI-коде).
Вопрос 3: Куда именно перемещается поток после вызова notify(): сразу на CPU, в EntryList или остаётся в WaitSet?
Ответ: Поток не начинает выполняться немедленно и даже не конкурирует сразу за процессор:
- Вызов
notify()выбирает один поток из_WaitSetи переносит его либо в_EntryList, либо в_cxq(в зависимости от внутренней HotSpot-политикиKnob_MoveToWaitSet). - Поток переходит из состояния
WAITINGв состояниеBLOCKED. - Поток, вызвавший
notify(), продолжает удерживать монитор до выхода из своегоsynchronized-блока! Только после вызоваmonitorexitосвобождается блокировка, и разбуженный поток получает возможность побороться за владение монитором наряду с новыми входящими потоками.
Вопрос 4: Как вызов дефолтного метода hashCode() влияет на производительность синхронизации объекта?
Ответ:
Вызов дефолтного hashCode() (identity hashcode) на объекте, захваченном тонкой блокировкой (Lightweight Lock), приводит к немедленному принудительному раздуванию (Lock Inflation) блокировки до тяжеловесного ObjectMonitor (Heavyweight Lock). Причина в том, что в тонком локе Mark Word занят указателем на Lock Record на стеке. Чтобы сохранить вычисленный identity hashcode навсегда без риска перезаписи, JVM вынуждена создать ObjectMonitor и сохранить хеш-код в его поле _header. Это приводит к деградации производительности из-за накладных расходов на создание C++ объекта и невозможности вернуться к легковесному локу.
3. Типичные ошибки и Red Flags
❌ “Монитор — это отдельный экземпляр класса java.lang.Object в Heap, который создаётся при запуске JVM.”
Правильно: монитор — это концептуальный механизм. В HotSpot заголовок любого объекта (Mark Word) содержит биты состояния блокировки. Системный объект ObjectMonitor создаётся в нативной памяти C++ только при эскалации до тяжёлой блокировки (Heavyweight Lock).
❌ “В Java 21 synchronized сначала проверяет Biased Locking.”
Правильно: Biased Locking был отключён по умолчанию в Java 15 и полностью удалён из JVM в Java 21 (JEP 416). Эскалация сразу начинается с Lightweight Locking.
❌ “Метод wait() освобождает все блокировки, которые держит текущий поток.”
Правильно: wait() освобождает только тот монитор, на объекте которого он был непосредственно вызван. Все остальные удерживаемые потоком мониторы остаются заблокированными, что часто является скрытой причиной трудноуловимых Deadlock.
❌ “Проверка условия перед wait() через if допустима, если в коде вызывается notify(), а не notifyAll().”
Правильно: Проверка условия ДОЛЖНА быть исключительно в цикле while (condition). Это фундаментальное требование для защиты от ложных пробуждений (Spurious Wakeups), разрешённых спецификацией POSIX threads и Java, а также для защиты от ситуаций, когда другой разбуженный поток успел изменить состояние ресурса раньше.