Что такое монитор (monitor) в Java?
Intrinsic (встроенный) monitor — каждый объект Java имеет монитор «из коробки». JVM автоматически связывает с каждым объектом структуру данных для синхронизации. Поэтому любой о...
Junior уровень
Базовое понимание
Монитор — это механизм синхронизации в Java, который обеспечивает взаимное исключение (mutual exclusion). Каждый объект в Java имеет свой встроенный (intrinsic) монитор.
Intrinsic (встроенный) monitor — каждый объект Java имеет монитор «из коробки». JVM автоматически связывает с каждым объектом структуру данных для синхронизации. Поэтому любой объект можно использовать как ключ блокировки.
Простая аналогия
Представьте переговорную комнату с одной дверью:
- Монитор = переговорная комната
- Ключ = блокировка (lock)
- Очередь снаружи = потоки, ждущие входа (EntryList)
- Комната ожидания = потоки, вызвавшие wait() (WaitSet)
Пример использования
public class Counter {
private int count = 0;
// synchronized использует монитор объекта 'this'
public synchronized void increment() {
count++; // Только один поток может быть здесь
}
public synchronized int getCount() {
return count;
}
}
Эквивалентные записи
// Вариант 1: synchronized метод
public synchronized void method() {
// code
}
// Вариант 2: synchronized блок (то же самое)
public void method() {
synchronized(this) { // Захват монитора объекта 'this'
// code
} // Освобождение монитора
}
wait() и notify()
Методы монитора для взаимодействия между потоками:
synchronized(lock) {
while (!condition) {
lock.wait(); // Освобождает монитор и ждёт
}
// Продолжает, когда notify() вызван
}
// В другом потоке:
synchronized(lock) {
condition = true;
lock.notify(); // Пробуждает один ждущий поток
}
Важные правила
| Правило | Описание |
|---|---|
| Только один владелец | Только один поток может владеть монитором одновременно |
| Реентерабельность | Владелец может повторно войти в монитор без блокировки |
| wait() освобождает | Вызов wait() временно освобождает монитор |
| Только внутри synchronized | wait()/notify() работают только внутри synchronized блока |
Middle уровень
Связь с Object Header (Mark Word)
Каждый объект в Java имеет заголовок, содержащий Mark Word:
Mark Word — часть object header (заголовка объекта) в JVM. Хранит информацию о блокировке: какой поток владеет, состояние (unlocked, biased, thin, fat). На 64-bit JVM = 8-12 байт.
|--------------------------------------------------------------|
| Object Header | |
| -------------------------------------------------------------- | ------------------------- |
| Mark Word (64/32 bit) | Class Pointer (64/32 bit) |
| -------------------------------------------------------------- | |
Mark Word хранит биты состояния блокировки:
| Биты (64-bit JVM) | Состояние |
|---|---|
01 |
No Lock / Biasable |
01 |
Biased Locking (с Thread ID) |
00 |
Lightweight Lock (Thin Lock) |
10 |
Heavyweight Lock (Fat Lock) |
11 |
Marked for GC |
Структура ObjectMonitor (C++ уровень JVM)
Когда монитор “раздут” до тяжёлого состояния, Mark Word содержит указатель на ObjectMonitor:
// Упрощённая структура из HotSpot
class ObjectMonitor {
void* _owner; // Поток-владелец
ObjectWaiter* _EntryList; // Очередь BLOCKED потоков
ObjectWaiter* _WaitSet; // Очередь WAITING потоков
int _Recursion; // Счётчик реентерабельности
int _WaitSetLock; // Защита WaitSet
};
Жизненный цикл потока в мониторе
┌─────────────────────────────────┐
│ │
▼ │
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐
│ NEW │→│ EntryList │→│ OWNER │→│ EXIT │
│ (создан) │ │ (BLOCKED) │ │(RUNNING) │ │ (освобождён) │
└──────────┘ └──────────────┘ └──────────┘ └──────────────┘
│
│ wait()
▼
┌──────────────┐
│ WaitSet │
│ (WAITING) │
└──────────────┘
│
│ notify()
▼
┌──────────────┐
│ EntryList │
│ (BLOCKED) │
└──────────────┘
Методы работы с монитором
| Метод | Описание |
|---|---|
monitor.enter() |
Захват монитора (блокирует если занят) |
monitor.exit() |
Освобождение монитора |
wait() |
Освобождает монитор и переходит в WAITING |
notify() |
Перемещает один поток из WaitSet в EntryList |
notifyAll() |
Перемещает все потоки из WaitSet в EntryList |
Lock Record на стеке
Lock Record — структура данных на стеке потока, которая хранит копию Mark Word при захвате блокировки. Нужна для восстановления Mark Word при освобождении (release).
Для лёгких блокировок JVM избегает создания тяжёлых ObjectMonitor:
- JVM копирует Mark Word объекта в Lock Record на стеке потока
- Поток использует быстрые CAS-операции для захвата
- Если конкуренции нет — всё остаётся на стеке (очень быстро)
- Если появляется конкуренция — происходит Lock Inflation до Heavyweight
Lock Inflation (раздувание блокировки) — переход от лёгкой блокировки (biased/thin) к тяжёлой (fat lock). Происходит при конкуренции: когда несколько потоков пытаются захватить один монитор.
Когда НЕ использовать мониторы
- High-throughput, write-heavy: миллионы операций/сек — lock-free алгоритмы (Atomic, CHM) быстрее
- Read-heavy нагрузка: ReentrantReadWriteLock позволяет читать параллельно, монитор — нет
- Нужен tryLock с таймаутом: монитор блокирует навсегда, ReentrantLock.tryLock(timeout) — нет
Senior уровень
Under the Hood: Эволюция состояний блокировки
JVM (HotSpot) использует стратегию эскалации блокировок (Lock Inflation):
1. No Lock (001)
|---------------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | bias:0 | 01 |
| --------------------------------------------------------------- | -------------------- | -------- | ----- | ------ | --- |
Объект не заблокирован. Mark Word содержит hash-код и возраст объекта (GC age).
2. Biased Locking (смещённая блокировка)
|-------------------------------------------------------------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | bias:1 | 01 |
| ------------------------------------------------------------------------- | ------- | -------- | ----- | ------ | --- |
- Если объект захватывается только одним потоком, JVM “смещает” его в сторону этого потока
- Поток заходит в synchronized без CAS, просто проверяя ID потока в заголовке
- Biased Locking — отключён по умолчанию с Java 15 (JEP 374), полностью удалён в Java 21 (JEP 416). В современных версиях Java мониторин проходит путь: No Lock → Thin Lock → Fat Lock.
3. Thin Lock (лёгкая блокировка)
|---------------------------------------------|
| thread:54 | lock record:6 | 00 |
| --------------------------------------------- | ------------- | --- |
- При появлении конкуренции используется CAS для подмены Mark Word на указатель на Lock Record
- Поток не засыпает — делает адаптивный спиннинг (несколько пустых циклов)
4. Fat Lock (тяжёлая блокировка)
|----------------------------------------------|
| object monitor pointer:62 | 10 |
| ---------------------------------------------- | --- |
- При высокой конкуренции создаётся полноценный
ObjectMonitor - Поток паркуется через ОС (
futexв Linux,WaitForSingleObjectв Windows) — состояние BLOCKED - Переключение контекста (Context Switch) — очень дорого (~1000+ ns)
Adaptive Spinning
HotSpot JVM запоминает историю блокировок:
// JVM отслеживает:
// - Сколько раз спиннинг был успешным на этом мониторе
// - Сколько раз поток сразу парковался
// Если спиннинг успешен — крутится дольше
// Если нет — паркует поток сразу
Это радикально улучшает производительность при кратковременных блокировках.
Monitor Inflation и Deflation
No Lock → Biased → Thin → Fat (Inflation — происходит автоматически)
Fat → Thin → ... (Deflation — только во время Safe Points / GC)
Много Fat Locks → производительность падает из-за context switches.
Оптимизации JIT-компилятора
Lock Coarsening (Укрупнение)
// До оптимизации:
synchronized(lock) { list.add(a); }
synchronized(lock) { list.add(b); }
synchronized(lock) { list.add(c); }
// После Lock Coarsening:
synchronized(lock) {
list.add(a);
list.add(b);
list.add(c);
}
Lock Elision (Удаление через Escape Analysis)
public void method() {
Object lock = new Object(); // Объект не "убегает" из метода
synchronized(lock) {
// JIT полностью удалит эту блокировку!
// Потому что никто другой не может получить доступ к lock
}
}
Производительность и Highload
Contention и EntryList
Если сотни потоков пытаются захватить один монитор:
EntryListпревращается в бутылочное горлышко- Потоки постоянно переключаются (context switching)
- CPU тратит больше времени на переключения, чем на работу
Решения:
- Сегментированные блокировки (как в
ConcurrentHashMap) - Использование
LongAdderвместоAtomicInteger - ReadWriteLock для преобладания чтения над записью
Диагностика
jstack -l
jstack -l <pid>
Вывод:
"worker-1" #15 prio=5 os_prio=0 tid=0x00007f... nid=0x1234 waiting for monitor entry
- waiting to lock <0x000000076af0c8d0> (a java.lang.Object)
- locked <0x000000076af0c8e0> (a java.util.HashMap)
"worker-2" #16 prio=5 os_prio=0 tid=0x00007f... nid=0x1235 waiting for monitor entry
- waiting to lock <0x000000076af0c8e0> (a java.util.HashMap)
JMC (Java Mission Control)
- Событие
Java Monitor Wait— время в wait() - Событие
Java Monitor Enter— время в BLOCKED - Показывает конкретные объекты с самой высокой конкуренцией
javap -c
javap -c MyClass.class
public void method();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // Захват монитора
4: ...
20: aload_1
21: monitorexit // Нормальный выход
22: goto 30
25: astore_2
26: aload_1
27: monitorexit // Выход при исключении
28: aload_2
29: athrow
30: return
Best Practices
- Минимизируйте область блокировки — держите synchronized блоки как можно короче
- Используйте private final Object lock — инкапсуляция, никто извне не захватит ваш лок
- Избегайте блокировок на уровне класса —
synchronized(ClassName.class)очень опасно - Рассмотрите ReadWriteLock — если чтение преобладает над записью
- Мониторьте contention через JMC — растущее время ожидания = проблема
- Избегайте synchronized в высоконагруженных системах — предпочтите lock-free структуры
🎯 Шпаргалка для интервью
Обязательно знать:
- Каждый объект Java имеет встроенный (intrinsic) монитор — механизм взаимного исключения
- Монитор = EntryList (очередь BLOCKED потоков) + WaitSet (очередь WAITING потоков) + счётчик реентерабельности
- Mark Word в object header хранит состояние блокировки: No Lock → Thin → Fat (эскалация)
- Biased Locking удалён в Java 21; современный путь: No Lock → Thin Lock → Fat Lock
- wait() временно освобождает монитор и переводит поток в WaitSet; notify() перемещает в EntryList
- JVM оптимизирует: Lock Coarsening (укрупнение), Lock Elision (удаление через Escape Analysis)
- При высокой конкуренции Thin Lock «раздувается» до Fat Lock с ObjectMonitor (context switch ~1000+ ns)
Частые уточняющие вопросы:
- Что происходит при вызове synchronized? — JVM пытается захватить монитор: сначала biased → thin (CAS + спиннинг) → fat (ObjectMonitor + парковка через ОС)
- Можно ли вызвать wait() без synchronized? — Нет, IllegalStateException — wait/notify работают только внутри синхронизированного блока
- Что такое Lock Record? — Структура на стеке потока с копией Mark Word; используется при Thin Locking
- Когда монитор НЕ подходит? — High-throughput write-heavy нагрузки; лучше lock-free (Atomic, ConcurrentHashMap)
Красные флаги (НЕ говорить):
- “Монитор — это отдельный объект в памяти” — нет, встроен в каждый объект (intrinsic)
- “Wait() освобождает монитор навсегда” — нет, временно; поток возвращается в EntryList после notify()
- “Synchronized всегда использует ObjectMonitor” — нет, при низкой конкуренции работает thin lock на стеке
- “Biased Locking всё ещё актуален в Java 21” — нет, полностью удалён (JEP 416)
Связанные темы:
- [[5. Как работает synchronized на уровне монитора]] — детальный алгоритм захвата монитора
- [[6. В чём разница между synchronized методом и synchronized блоком]] — разные способы использования монитора
- [[7. Что такое reentrant lock]] — ReentrantLock как альтернатива встроенному монитору
- [[1. В чём разница между synchronized и volatile]] — когда нужен монитор, а когда volatile