Объясните механизм повторного входа одного потока в уже захваченный им монитор synchronized.

Объясните механизм повторного входа одного потока в уже захваченный им монитор synchronized.

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Мониторы, используемые synchronized, являются реентерабельными: поток, уже владеющий монитором, может повторно захватить тот же монитор без взаимной блокировки. Каждый повторный вход увеличивает внутренний счётчик удержаний, а выход уменьшает его; монитор становится доступен другим потокам только после полного выхода.

Реентерабельность относится только к тому же потоку и тому же объекту-монитору. Она не предотвращает блокировку, если поток захватывает другие мониторы в конфликтующем порядке.

Исторический контекст

Синхронизация на встроенных мониторах Java предназначена для защиты критических секций и координации доступа к общему состоянию. Без реентерабельности вызов одного синхронизированного метода из другого синхронизированного метода того же объекта приводил бы к самоблокировке.

Реентерабельность позволяет безопасно строить вложенные вызовы: общий внутренний метод может быть синхронизирован независимо от того, вызван ли он напрямую или из уже защищённой операции.

Постановка проблемы

Поток может войти в синхронизированный метод, а затем вызвать другой синхронизированный метод того же объекта. Оба метода используют один монитор — обычно монитор this.

Если бы повторный захват уже принадлежащего потоку монитора блокировал этот же поток, выполнение остановилось бы навсегда. При этом другие потоки всё равно не смогли бы продолжить работу, потому что монитор оставался бы захваченным.

Подробное решение

При входе в synchronized поток захватывает монитор объекта. Если монитор уже принадлежит этому же потоку, JVM не передаёт поток в состояние ожидания, а увеличивает счётчик повторных захватов.

Каждый успешный вход должен иметь соответствующий выход. Поэтому после выхода из внутреннего синхронизированного участка монитор всё ещё удерживается внешним участком. Только последний выход, соответствующий исходному захвату, освобождает монитор для других потоков.

Например:

class Account { synchronized void transfer() { validate(); } synchronized void validate() { System.out.println("Проверка"); } }

При вызове transfer() поток захватывает монитор объекта Account. Вызов validate() повторно захватывает тот же монитор, выполняется без блокировки, а после возврата управление остаётся внутри внешней критической секции.

Реентерабельность не означает, что синхронизация становится безопасной автоматически. Вызов внешнего кода, например listener или callback, под удерживаемым монитором может привести к длительным блокировкам, взаимной блокировке с другим монитором или неожиданной конкуренции за тот же объект.

Важно также различать монитор объекта и монитор класса. Синхронизированный экземплярный метод использует конкретный объект, а static synchronized-метод — объект Class. Повторный вход безопасен только при совпадении владельца и самого монитора.

Компромисс встроенного монитора — простой синтаксис и автоматическое освобождение при выходе из блока, включая выход из-за исключения. Ограничение — нельзя напрямую управлять тайм-аутом захвата или прерывать ожидание так, как это позволяет ReentrantLock.

Ситуация из практики

Сервис хранит состояние и уведомляет подписчиков об изменении. Один из подписчиков при получении уведомления вызывает обратно метод сервиса, который читает это состояние. Если уведомление выполняется внутри synchronized-метода, обратный вызов тем же потоком не зависнет именно благодаря реентерабельности.

Рассматриваются варианты:

  • оставить callback внутри монитора — просто, но внешний код может выполняться долго, вызвать другой сервис и создать риск взаимной блокировки;
  • заменить synchronized на ReentrantLock — появляются тайм-ауты и дополнительные возможности, но увеличивается сложность и риск неправильного освобождения;
  • сформировать снимок состояния под монитором, а уведомление выполнить после выхода — callback не удерживает внутренний монитор, но подписчик получает согласованный снимок, а не постоянно меняющееся состояние.

Обычно выбирают третий вариант: критическая секция остаётся короткой, а внешний код не вызывается под внутренним монитором. Реентерабельность всё равно полезна для внутренних вызовов, но не должна быть основанием для удержания блокировки во время произвольных callback-вызовов.

Что кандидаты часто упускают

  1. Увеличивает ли повторный вход число разрешений для других потоков?

    Нет. Повторный вход увеличивает счётчик удержаний, но не создаёт дополнительные права доступа для других потоков. Другой поток сможет захватить монитор только после того, как текущий поток выйдет из всех вложенных синхронизированных участков, использующих этот монитор.

  2. Работает ли реентерабельность при захвате двух разных объектов?

    Нет, она действует только для того же монитора. Если поток удерживает монитор объекта A и пытается захватить монитор объекта B, он может заблокироваться на B. Одновременно другой поток может удерживать B и ждать A — это классический сценарий взаимной блокировки.

  3. Устраняет ли реентерабельность проблемы видимости данных между потоками?

    Нет, она решает только повторный захват монитора тем же потоком. Вход в synchronized после выхода другого потока из синхронизированного участка участвует в отношениях happens-before, поэтому корректно используется для публикации изменений, но сам факт реентерабельности не делает несинхронизированные обращения к общим данным безопасными.