Короткий ответ
Потому что эти методы работают с монитором объекта, а не с потоком: поток ожидает на конкретном объекте-«замке», и уведомление тоже отправляется через объект. Встроенный монитор есть у любого наследника Object, поэтому естественно разместить методы ожидания и уведомления в самом Object, чтобы их можно было вызывать для любого объекта-монитора.
Как это работает подробнее
Механика такая: поток вызывает someObject.wait() из синхронизированного блока по someObject, освобождает его монитор и засыпает, а другой поток вызывает notify()/notifyAll() на том же объекте, чтобы разбудить ожидающих. Именно привязка к объекту позволяет независимо синхронизироваться на разных «замках»: ожидание на одном объекте не мешает потокам работать с другим.
notify() будит один произвольный поток из очереди ожидания на мониторе, notifyAll() — все ожидающие, после чего они конкурируют за монитор. notifyAll() безопаснее в большинстве сценариев: он снижает риск «потерянного уведомления».
Пример кода
import java.util.Queue;
void consume(Queue<Object> queue) throws InterruptedException {
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait(); // монитор queue отпущен, ждём уведомления
}
Object item = queue.poll();
queue.notifyAll(); // будим производителей
}
}Что использовать на практике
- Вызывай
wait()/notify()только внутри synchronized-блока по тому же объекту-монитору. - Проверяй условие в цикле
whileи предпочитайnotifyAll()— это защищает от «потерянного уведомления» и ложных пробуждений.