Object not locked by thread before notify: разбираем ошибку IllegalMonitorStateException

Ошибка IllegalMonitorStateException: object not locked by thread before notify возникает в Java-приложении в тот момент, когда поток вызывает метод notify(), notifyAll() или wait() на объекте, монитор которого он не захватил. JVM требует, чтобы перед вызовом этих методов поток владел блокировкой объекта — то есть находился внутри блока synchronized по этому же объекту. Если правило нарушено, виртуальная машина немедленно бросает исключение, и поток завершает выполнение с ошибкой.

Проблема особенно коварна в многопоточном коде: с виду синхронизация может присутствовать, но блокироваться не тот объект, на котором вызывается notify(). В этой статье разберём механизм мониторов в Java, типичные сценарии появления исключения и рабочие способы его устранения — от классического synchronized/wait/notify до современных примитивов из пакета java.util.concurrent.

Что означает ошибка object not locked by thread before notify

Каждый объект в Java имеет ассоциированный с ним монитор (intrinsic lock). Когда поток входит в блок synchronized(obj), он захватывает монитор объекта obj. Только владелец монитора имеет право вызывать на этом объекте методы wait(), notify() и notifyAll() — таково требование спецификации JVM.

Текст исключения читается дословно: «объект не заблокирован потоком перед вызовом notify». Иными словами, JVM проверила владение монитором в момент вызова и обнаружила, что текущий поток им не владеет. Стоит понимать, что это проверка времени выполнения, а не компиляции — код скомпилируется без единого предупреждения, и ошибка проявится только при реальном запуске, часто в непредсказуемый момент.

Исключение IllegalMonitorStateException относится к unchecked-исключениям (наследник RuntimeException), поэтому компилятор не обязует его перехватывать. Это ещё одна причина, почему баг легко пропустить на этапе разработки и обнаружить уже в продакшене.

Типичные сценарии возникновения исключения

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

  • 🔓 Вызов notify() или wait() вообще вне блока synchronized — самый частый случай у начинающих разработчиков.
  • 🎯 Синхронизация по одному объекту, а вызов notify() — на другом, например блокировка по this, а уведомление на объекте-очереди.
  • 🔁 Синхронизация по изменяемому полю: объект блокировки перезаписывается, и поток вызывает notify() уже на новом экземпляре, которым не владеет.
  • 🧵 Использование notify() внутри ReentrantLock без Condition — монитор объекта не захватывается, хотя блокировка формально есть.

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

📊 Где вы столкнулись с IllegalMonitorStateException?
В учебном/тестовом коде
В многопоточном продакшен-коде
При работе со старым legacy-кодом
При изучении wait/notify

Как правильно использовать wait и notify

Базовое правило формулируется просто: вызов wait/notify допустим только внутри synchronized-блока по тому же объекту. Поток, желающий уведомить других, обязан сначала захватить монитор объекта-уведомителя, выполнить notify() или notifyAll() и лишь затем выйти из блока, освободив монитор.

Корректный шаблон выглядит следующим образом:

synchronized (lock) {

while (!condition) {

lock.wait();

}

// обработка после пробуждения

}

synchronized (lock) {

condition = true;

lock.notifyAll();

}

Обратите внимание на цикл while вместо if вокруг wait(). Поток может проснуться ложно (spurious wakeup) или из-за notifyAll(), предназначенного не ему, поэтому условие нужно перепроверять после каждого пробуждения. Это не относится напрямую к IllegalMonitorStateException, но без такого паттерна корректно синхронизированный код всё равно будет работать с ошибками.

Пошаговая диагностика проблемы

Когда исключение уже поймано в логах, действуйте системно. Сначала найдите в стектрейсе точную строку кода, где вызван notify() или wait(), — JVM указывает класс и номер строки. Затем проследите, какой объект используется как монитор в ближайшем оборачивающем synchronized, и сравните его с объектом вызова.

☑️ Проверка кода с wait/notify

Выполнено: 0 / 5

Если синхронизация внешне корректна, проверьте, не создаётся ли объект блокировки заново между захватом монитора и вызовом notify(). Поле-замок должно быть объявлено как private final Object lock = new Object(); — это защищает от случайной подмены объекта синхронизации. Также убедитесь, что метод не вызывается из лямбд или вложенных классов, где this ссылается на другой экземпляр, чем ожидалось.

⚠️ Внимание: синхронизация по публичному объекту (например, по this класса, доступного извне) позволяет стороннему коду захватывать тот же монитор. Это может приводить не только к IllegalMonitorStateException, но и к взаимным блокировкам. Предпочтительный подход — приватный final-объект-замок.

Сравнение подходов к синхронизации

Классическая связка synchronized/wait/notify — не единственный способ организовать ожидание и уведомление потоков. В таблице ниже сопоставлены основные механизмы, доступные в стандартной библиотеке Java.

Механизм API уведомления Риск IllegalMonitorStateException Гибкость
synchronized + wait/notify wait(), notify(), notifyAll() Высокий при нарушении владения монитором Низкая: один набор ожидающих потоков на объект
ReentrantLock + Condition await(), signal() Исключение возможно, но структура кода явнее Высокая: несколько Condition на один замок
BlockingQueue put(), take() (встроенная блокировка) Отсутствует — синхронизация скрыта внутри Готовый паттерн producer-consumer
CountDownLatch / Semaphore countDown(), release() Отсутствует Ограниченные сценарии координации

Из сравнения видно: чем выше уровень абстракции, тем меньше возможностей ошибиться с владением монитором. Для новых проектов классы из java.util.concurrent почти всегда предпочтительнее ручного управления через wait/notify.

Альтернатива: Lock и Condition

Если по какой-то причине нельзя использовать готовые блокирующие структуры, замените связку synchronized/wait/notify на ReentrantLock с объектом Condition. Здесь владение блокировкой контролируется явными вызовами lock() и unlock(), а ожидание и уведомление привязаны к конкретному условию, что делает код читабельнее и снижает риск ошибок.

lock.lock();

try {

while (!condition) {

conditionVar.await();

}

// работа после пробуждения

} finally {

lock.unlock();

}

Здесь тоже есть своё исключение при нарушении контракта: вызов await() или signal() без удержания замка приводит к тому же IllegalMonitorStateException. Однако благодаря явному lock()/unlock() и обязательному finally структура владения видна прямо в коде, и ошибку проще заметить на ревью. Пара Condition на один замок дополнительно позволяет разделять очереди ожидающих потоков — то, чего классический notify() не умеет.

Почему wait() нельзя вызывать без synchronized

Метод wait() заставляет поток освободить монитор объекта и встать в очередь ожидания. Если поток не владеет монитором, освобождать нечего — операция бессмысленна, поэтому JVM сразу бросает IllegalMonitorStateException. Аналогично notify() будит поток из очереди ожидания объекта, а управлять этой очередью разрешено только владельцу монитора — иначе возможны гонки при изменении состояния очереди.

Отладка и профилактика ошибок синхронизации

Для поиска проблемных мест в большом проекте полезен дамп потоков. Команда jstack <pid> показывает состояние всех потоков и удерживаемые ими мониторы — по этому выводу можно понять, кто владеет нужной блокировкой, а кто её ожидает. В IDE точки останова на строке вызова notify() позволяют проверить фактическое владение монитором в момент выполнения.

⚠️ Внимание: не пытайтесь «заглушить» исключение обёртыванием в try-catch без исправления логики. Перехваченный IllegalMonitorStateException означает, что уведомление не доставлено — ожидающий поток может зависнуть навсегда, и вы получите взаимную блокировку вместо явной ошибки.

Профилактика сводится к дисциплине: выделяйте отдельный private final объект-замок, документируйте, какие методы требуют удержания блокировки, и по возможности переносите координацию потоков на готовые классы java.util.concurrent. Статические анализаторы кода также способны пометить вызовы wait/notify вне синхронизированного контекста — имеет смысл включить соответствующие проверки в вашем инструменте анализа.

Часто задаваемые вопросы

Чем отличается notify() от notifyAll()?

notify() будит один произвольный поток из очереди ожидания объекта, а notifyAll() — все ожидающие потоки. Если очередь разделяют потоки с разными условиями пробуждения, notify() может разбудить «не того» потока, и уведомление будет потеряно. Поэтому в сомнительных случаях безопаснее notifyAll().

Можно ли поймать IllegalMonitorStateException и продолжить работу?

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

Почему ошибка возникает только иногда, а не при каждом запуске?

Многопоточные ошибки зависят от порядка планирования потоков. Если ветка с вызовом notify() обычно успевает захватить монитор корректно, а в редких случаях — нет (например, из-за гонки при инициализации объекта блокировки), исключение будет плавающим. Такие сбои воспроизводятся чаще под нагрузкой или на другом оборудовании.

Заменит ли ReentrantLock все случаи использования wait/notify?

Функционально — да: пара ReentrantLock + Condition покрывает те же сценарии и добавляет возможности вроде нескольких условий ожидания и прерываемого ожидания. Однако для простых задач ещё проще использовать готовые структуры — BlockingQueue, CountDownLatch или CompletableFuture, где синхронизация скрыта внутри и ошибиться с мониторами невозможно.

Влияет ли версия Java на поведение этой ошибки?

Контракт владения монитором зафиксирован в спецификации JVM и не меняется между версиями: вызов wait/notify без владения монитором всегда приводит к IllegalMonitorStateException. Меняться может лишь формат сообщения об ошибке и детализация стектрейса.