ГлавнаяБлог › synchronized и мониторы: как JVM управляет блокировками

synchronized и мониторы: как JVM управляет блокировками

Что такое monitor

Каждый объект в Java потенциально может быть монитором — структурой для взаимного исключения. Когда поток входит в synchronized блок, он пытается захватить монитор, ассоциированный с объектом. Монитор хранит owner-поток, счётчик реентрантности и очереди ожидающих потоков (entry list и wait set).

synchronized — это не библиотечный примитив, а часть байткода: инструкции monitorenter и monitorexit. Для синхронизированных методов JVM просто помечает метод флагом ACC_SYNCHRONIZED, и захват/отпускание монитора делает сама JVM при входе/выходе.

Markword и три состояния блокировки

Информация о блокировке живёт в заголовке объекта — markword (обычно 8 байт на 64-битной JVM). У современных HotSpot есть несколько состояний lock:

  • Neutral / unlocked — объект свободен, в markword лежит hashcode и биты состояния.
  • Biased locking — оптимизация под случай, когда объект почти всегда захватывается одним и тем же потоком (типично для однопоточного кода). В markword записывается thread id, и повторный захват — это просто проверка id без CAS. Начиная с Java 15 biased locking отключён по умолчанию и в Java 18+ удалён — оптимизация не оправдала себя на современных многоядерных нагрузках.
  • Thin lock (lightweight) — при конкуренции без реальных потоков в очереди захват идёт через CAS: поток пытается атомарно записать указатель на свой lock record в markword. Это дёшево, без похода в ядро ОС.
  • Fat lock (heavyweight) — если на объект претендуют несколько потоков одновременно, thin lock «раздувается» в полноценный ObjectMonitor с очередями в куче, и неудачники уходят в блокировку через OS-примитивы (futex на Linux). Это дорого: контекст-свитч, поход в ядро.

Именно поэтому synchronized в современной JVM почти бесплатен при низкой конкуренции — деградация в дорогой fat lock происходит только при реальной борьбе за ресурс.

Представь дверь в переговорку. Biased lock — это когда дверь всегда открывает один и тот же человек, и охрана его просто узнаёт в лицо. Thin lock — очередь из двух-трёх человек, которые быстро договариваются между собой кто заходит. Fat lock — это когда набирается толпа, и приходится ставить настоящего охранника с журналом записи и залом ожидания.

Реентрантность

Монитор считает, сколько раз владелец-поток вошёл в блок повторно (recursion count). Поэтому один и тот же поток может свободно войти в несколько вложенных synchronized блоков на одном объекте — блокировка не заклинит сама себя. Выход из внешнего блока произойдёт только когда счётчик дойдёт до нуля.

public synchronized void outer() {
    inner(); // тот же поток, тот же монитор — ок
}
public synchronized void inner() {
    // ...
}

wait/notify и связь с монитором

Object.wait(), notify(), notifyAll() работают только с монитором объекта, на котором вызваны, и обязаны вызываться внутри synchronized-блока на этом же объекте — иначе IllegalMonitorStateException. wait() отпускает монитор и кладёт поток в wait set; notify() перемещает один поток из wait set обратно в entry list, но монитор он получит не сразу, а только когда текущий владелец выйдет из блока.

Частая ошибка — вызывать notify() без цикла проверки условия у ждущего потока. Между notify() и реальным пробуждением условие может измениться (другой поток успел вклиниться), поэтому wait() всегда нужно оборачивать в while(!condition) wait();, а не в if.

synchronized vs ReentrantLock

ReentrantLock из java.util.concurrent.locks даёт то, что не умеет synchronized: tryLock() с таймаутом, прерываемое ожидание, справедливый режим (fair), несколько условий (Condition) на один лок вместо одного wait set. Плата — явный lock()/unlock(), обязательно в try/finally, где легко забыть unlock и получить вечную блокировку.

Lock lock = new ReentrantLock();
lock.lock();
try {
    // критическая секция
} finally {
    lock.unlock();
}

На практике: если нужна простая взаимоисключающая секция — бери synchronized, JIT и JVM оптимизируют его не хуже. Если нужны таймауты, честность очереди или несколько условий ожидания — ReentrantLock.

Почему synchronized считается медленным, хотя современные бенчмарки это не подтверждают?
Репутация тянется из Java 1.4-5, где любой synchronized сразу шёл в heavyweight lock с системным вызовом. С появлением thin lock и адаптивного спиннинга в HotSpot цена низкая при отсутствии реальной конкуренции — раздувание до fat lock происходит только при борьбе нескольких потоков.
Что произойдёт, если один поток вызовет wait(), а другой notifyAll() на другом объекте?
Ничего не произойдёт — notify/wait работают строго в рамках монитора конкретного объекта. Разбудить поток, ждущий на объекте A, можно только вызовом notify/notifyAll на этом же объекте A.

Ещё разборы