Исключение java.lang.IllegalMonitorStateException: object not locked by thread before wait возникает в тот момент, когда поток вызывает метод wait() на объекте, монитор которого он не захватил — то есть вызов выполнен вне блока synchronized по этому же объекту. JVM проверяет владение монитором в рантайме, и если текущий поток не является владельцем, выполнение прерывается именно этим сообщением.

Это одна из самых частых ошибок у тех, кто начинает работать с многопоточностью в Java и механизмом wait/notify. Хорошая новость: причина почти всегда одна и та же, а исправление сводится к правильной организации синхронизации. Ниже разберём механику ошибки, типичные сценарии и корректные паттерны кода.

Что означает эта ошибка на уровне JVM

Каждый объект в Java имеет связанный с ним монитор (intrinsic lock). Методы wait(), notify() и notifyAll() определены в классе Object, и их контракт требует: вызывающий поток обязан владеть монитором объекта, на котором вызывается метод. Владение достигается входом в блок synchronized(obj) или вызовом synchronized-метода этого объекта.

Когда поток вызывает wait() без захваченного монитора, виртуальная машина не может выполнить операцию — ведь wait() должен освободить монитор, которого у потока нет. Результат — IllegalMonitorStateException с текстом про объект, не заблокированный потоком перед ожиданием.

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

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

Чаще всего проблема сводится к нескольким повторяющимся паттернам. Распознав свой случай, вы сразу поймёте, что исправлять.

  • 🔓 Вызов wait() без synchronized — самый частый случай: разработчик просто пишет obj.wait() в обычном методе, забыв обернуть вызов в блок синхронизации.
  • 🔀 Синхронизация по другому объекту — блок synchronized(lockA) есть, но wait() вызывается на lockB. Мониторы разные, владения нет.
  • 🧵 Ожидание на объекте потока — вызов thread.wait() вместо правильной работы через join() или отдельный объект-заглушку.
  • 📦 Синхронизация по изменяемому полю — объект-блокировка перезаписывается между захватом монитора и вызовом wait(), и поток оказывается «не владельцем» нового экземпляра.
📊 Где вы столкнулись с IllegalMonitorStateException?
Вызвал wait() без synchronized
Синхронизировался по другому объекту
Работал с wait/notify на объекте Thread
При использовании сторонней библиотеки

Как выглядит проблемный и правильный код

Рассмотрим минимальный пример, который гарантированно бросает исключение:

public class Demo {

private final Object lock = new Object();

public void badWait() throws InterruptedException {

lock.wait(); // IllegalMonitorStateException:

// object not locked by thread before wait

}

}

Исправление — обернуть вызов в synchronized по тому же объекту. Причём проверку условия ожидания нужно делать в цикле while, а не через if: это защищает от ложных пробуждений (spurious wakeup), которые допускает спецификация JVM.

public class Demo {

private final Object lock = new Object();

private boolean ready = false;

public void goodWait() throws InterruptedException {

synchronized (lock) {

while (!ready) {

lock.wait(); // монитор захвачен — всё корректно

}

}

}

public void signalReady() {

synchronized (lock) {

ready = true;

lock.notifyAll();

}

}

}

💡

Правило простое: wait(), notify() и notifyAll() вызываются только внутри synchronized по тому же самому объекту, а условие ожидания проверяется в цикле while.

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

Если ошибка возникает в реальном приложении, а не в учебном примере, действуйте последовательно. Сначала откройте стек-трейс и найдите строку вашего кода, где брошено исключение — верхние фреймы будут внутри Object.wait(), вам нужен первый фрейм из собственных классов.

Дальше проверьте, какой именно объект используется для синхронизации вокруг этого вызова. Объект в synchronized и объект, на котором вызван wait(), обязаны быть одним и тем же экземпляром — не просто объектами одного класса, а одной ссылкой. Особенно внимательно отнеситесь к полям, которые где-то переприсваиваются.

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

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

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

Механизм wait/notify — низкоуровневый инструмент. В современном коде его часто заменяют конструкциями из пакета java.util.concurrent, которые менее подвержены подобным ошибкам. Сравнение подходов:

ПодходРиск IllegalMonitorStateExceptionСложностьКогда уместен
wait/notify + synchronizedВысокий при неверном порядке вызововТребует дисциплиныУчебные задачи, легаси-код
ReentrantLock + ConditionНизкий — await() требует явного lock()СредняяГибкие условия ожидания
BlockingQueueПрактически отсутствуетНизкаяПаттерн producer–consumer
CountDownLatch / SemaphoreОтсутствуетНизкаяОдноразовая синхронизация потоков
⚠️ Внимание: при переходе на ReentrantLock не забудьте освобождать блокировку в блоке finally. Вызов condition.await() без предварительного lock.lock() тоже приведёт к IllegalMonitorStateException — контракт здесь аналогичный.

Особые случаи и подводные камни

Отдельного внимания заслуживает ситуация с синхронизацией по объектам-обёрткам и строкам. Строковые литералы интернируются JVM, поэтому synchronized("lock") в разных классах может неожиданно захватывать один и тот же монитор — или, наоборот, вы можете синхронизироваться по одной строке, а вызывать wait() на равной по значению, но другой ссылке. Для блокировок используйте выделенный private final Object.

Ещё один тонкий момент — вызов wait() на объекте Thread. Внутри реализации join() уже используется wait() на объекте потока, и самостоятельное вмешательство в этот механизм приводит к непредсказуемому поведению. Для ожидания завершения потока применяйте join(), а для обмена сигналами — собственный объект-заглушку.

Почему wait() освобождает монитор?

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

💡

Включите в IDE инспекции для многопоточного кода — например, IntelliJ IDEA подсвечивает вызовы wait()/notify() вне синхронизированного контекста ещё до запуска программы.

FAQ: частые вопросы

Можно ли вызвать wait() в synchronized-методе?

Да. Синхронизированный метод экземпляра захватывает монитор this, поэтому this.wait() (или просто wait()) внутри него корректен. Для static synchronized методов монитором служит объект класса, и wait() будет работать с ним.

Чем IllegalMonitorStateException отличается от InterruptedException?

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

Почему условие ожидания нужно проверять в while, а не в if?

Спецификация допускает ложные пробуждения, когда wait() завершается без notify(). Кроме того, после пробуждения условие могло снова стать ложным из-за действий другого потока. Цикл while гарантирует повторную проверку перед продолжением работы.

Ошибка возникает в коде сторонней библиотеки — что делать?

Сначала убедитесь, что причина не в вашем коде, который передаёт библиотеке некорректно используемые объекты или нарушает её контракты потокобезопасности. Если проблема внутри библиотеки — обновитесь до актуальной версии и проверьте её трекер ошибок; известные дефекты синхронизации обычно исправляются мейнтейнерами.

Заменит ли synchronized на ReentrantLock автоматически?

Нет, механическая замена не поможет: у Lock свой контракт — await() на объекте Condition требует удержания соответствующей блокировки. Нужно перепроектировать участок кода с учётом нового API, а не просто подменить ключевые слова.