Питання 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