Исключение 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?