Що таке монітор (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