Вопрос 4 · Раздел 9

Что такое монитор (monitor) в Java?

Intrinsic (встроенный) monitor — каждый объект Java имеет монитор «из коробки». JVM автоматически связывает с каждым объектом структуру данных для синхронизации. Поэтому любой о...

Версии по языкам: English Russian Ukrainian

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:

  1. JVM копирует Mark Word объекта в Lock Record на стеке потока
  2. Поток использует быстрые CAS-операции для захвата
  3. Если конкуренции нет — всё остаётся на стеке (очень быстро)
  4. Если появляется конкуренция — происходит Lock Inflation до Heavyweight

Lock Inflation (раздувание блокировки) — переход от лёгкой блокировки (biased/thin) к тяжёлой (fat lock). Происходит при конкуренции: когда несколько потоков пытаются захватить один монитор.

Когда НЕ использовать мониторы

  1. High-throughput, write-heavy: миллионы операций/сек — lock-free алгоритмы (Atomic, CHM) быстрее
  2. Read-heavy нагрузка: ReentrantReadWriteLock позволяет читать параллельно, монитор — нет
  3. Нужен 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

  1. Минимизируйте область блокировки — держите synchronized блоки как можно короче
  2. Используйте private final Object lock — инкапсуляция, никто извне не захватит ваш лок
  3. Избегайте блокировок на уровне классаsynchronized(ClassName.class) очень опасно
  4. Рассмотрите ReadWriteLock — если чтение преобладает над записью
  5. Мониторьте contention через JMC — растущее время ожидания = проблема
  6. Избегайте 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