Object.wait(): примеры (JAVA)

Метод Object.wait() в Java - описание и сценарии использования
Раздел: Многопоточность, Синхронизация
Object.wait(): void

Общее описание Object.wait()

Метод Object.wait() - базовый механизм ожидания в Java, встроенный в класс java.lang.Object. Он переводит текущий поток в состояние ожидания до тех пор, пока другой поток не пробудит его с помощью notify() или notifyAll(), либо пока не истечет заданный таймаут, либо пока поток не будет прерван. Метод освобождает захваченный монитор объекта на время ожидания и повторно захватывает его перед возвратом.

Сигнатуры:

  • public final void wait() throws InterruptedException
  • public final void wait(long timeout) throws InterruptedException
  • public final void wait(long timeout, int nanos) throws InterruptedException

Параметры и поведение:

  • wait(): ожидает неограниченно до notify/notifyAll/interrupt.
  • wait(long timeout): ожидает не более timeout миллисекунд. Если timeout < 0, выбрасывается IllegalArgumentException. timeout == 0 интерпретируется как ожидание без таймаута.
  • wait(long timeout, int nanos): ожидает не более timeout миллисекунд + nanos наносекунд. nanos должен быть в диапазоне 0..999999, иначе IllegalArgumentException. Если timeout == 0 и nanos == 0 - аналогично бесконечному ожиданию.

Возвращаемое значение: метод возвращает void. Причины выхода из ожидания:

  • получено уведомление через notify() или notifyAll();
  • текущее ожидание прервано - InterruptedException;
  • истек таймаут (для версий с параметрами);
  • возможны спонтанные пробуждения (spurious wakeups), поэтому рекомендуется проверять условие в цикле.

Исключения и требования:

  • IllegalMonitorStateException - если метод вызван без владения монитором объекта (не в synchronized блоке/методе).
  • InterruptedException - если поток прерван во время ожидания.
  • IllegalArgumentException - при некорректных значениях timeout или nanos (см. выше).

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

Короткие примеры использования

1) Простейший пример wait/notify.

public class SimpleWaitNotify {
    private final Object lock = new Object();

    public void runExample() throws InterruptedException {
        Thread waiter = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("waiter: перед wait");
                    lock.wait();
                    System.out.println("waiter: после wait");
                } catch (InterruptedException e) {
                    System.out.println("waiter: прерван");
                }
            }
        });

        Thread notifier = new Thread(() -> {
            synchronized (lock) {
                System.out.println("notifier: перед notify");
                lock.notify();
                System.out.println("notifier: после notify");
            }
        });

        waiter.start();
        Thread.sleep(100); // дать время waiter зайти в wait
        notifier.start();
        waiter.join();
        notifier.join();
    }

    public static void main(String[] args) throws Exception {
        new SimpleWaitNotify().runExample();
    }
}
waiter: перед wait
notifier: перед notify
notifier: после notify
waiter: после wait

2) Timeout-вариант.

synchronized (lock) {
    System.out.println("до wait с таймаутом 500 мс");
    lock.wait(500);
    System.out.println("после wait (либо notify, либо таймаут)");
}
до wait с таймаутом 500 мс
после wait (либо notify, либо таймаут)

3) Пример с nanos (короткий демонстративный):

synchronized (lock) {
    lock.wait(1, 500_000); // 1 мс + 500_000 нс = 1.5 мс
}
(поведение аналогично ожиданию с очень маленьким таймаутом)

4) Ошибка вызова без синхронизации.

Object o = new Object();
o.wait(); // приведет к исключению
Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.base/java.lang.Object.wait(Native Method)
    ...

Похожие механизмы в Java

В Java имеются более высокоуровневые и гибкие средства синхронизации:

  • java.util.concurrent.locks.Condition - применяется вместе с ReentrantLock. Позволяет ждать с той же семантикой, но предоставляет отдельные очереди ожидания и явное управление блокировкой. Предпочтителен при сложной логике и при использовании ReentrantLock.
  • LockSupport.park()/unpark() - низкоуровневый блокирующий механизм для построения своих примитивов блокировки; не требует владения монитором и работает на уровне потоков.
  • Высокоуровневые структуры (Semaphore, CountDownLatch, CyclicBarrier, BlockingQueue) - заменяют явное wait/notify в большинстве типовых сценариев (производитель-потребитель, ожидание завершения и т.п.).

Когда использовать что:

  • Object.wait()/notify() - подойдет для простых случаев и при необходимости совместимости с существующим кодом, но требует аккуратного обращения с монитором.
  • Condition - при необходимости гибкой, выразительной и безопасной работы с блокировками.
  • BlockingQueue и другие утилиты - когда требуется готовая потокобезопасная структура без риска ошибок с notify/notifyAll.

Аналоги в других языках и заметки

Ниже краткие соответствия и отличия с примерами.

Python (threading.Condition)

import threading
cond = threading.Condition()

def waiter():
    with cond:
        print('py: до wait')
        cond.wait()
        print('py: после wait')

threading.Thread(target=waiter).start()
# в другом потоке: with cond: cond.notify()
py: до wait
py: после wait

C# (Monitor.Wait / Monitor.Pulse)

using System;
using System.Threading;

object o = new object();
new Thread(() => {
    lock (o) {
        Console.WriteLine("cs: до Wait");
        Monitor.Wait(o);
        Console.WriteLine("cs: после Wait");
    }
}).Start();
// в другом потоке: lock(o){ Monitor.Pulse(o); }
cs: до Wait
cs: после Wait

Go (sync.Cond)

var mu sync.Mutex
cond := sync.NewCond(&mu)

go func() {
    mu.Lock()
    fmt.Println("go: до Wait")
    cond.Wait()
    fmt.Println("go: после Wait")
    mu.Unlock()
}()
// в другом горутине: cond.Signal()
go: до Wait
go: после Wait

JavaScript (Atomics.wait на SharedArrayBuffer в среде, где поддерживается)

// только в средах, где доступен Atomics.wait (Worker + SharedArrayBuffer)
// Atomics.wait(typedArray, index, value, timeout)
возвращает 'ok' или 'timed-out' или 'not-equal'

Lua (корутины)

co = coroutine.create(function()
  print('lua: до yield')
  coroutine.yield()
  print('lua: после resume')
end)
coroutine.resume(co)
coroutine.resume(co)
lua: до yield
lua: после resume

PHP: в стандартной среде отсутствует прямой эквивалент; в pthreads или при использовании внешних процессов применяются другие подходы (семафоры, очереди, блокировки в расширениях).

Отличия от Java:

  • Во многих языках ожидание привязано не к любому объекту, а к специализированным структурам (Condition, Event, Channel).
  • Java требует владения монитором у вызывающего; в других средах это требование может отсутствовать.
  • В Java часто используются высокоуровневые структуры вместо прямого использования wait/notify.

Типичные ошибки при использовании

1) Вызов wait без владения монитором - IllegalMonitorStateException.

public class BadWait {
    public static void main(String[] args) throws Exception {
        Object o = new Object();
        o.wait(); // ошибка
    }
}
Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.base/java.lang.Object.wait(Native Method)
    ...

2) Игнорирование InterruptedException - потеря информации о прерывании. Рекомендуется корректно обрабатывать прерывания или повторно прерывать поток.

3) Использование if вместо while при ожидании условия - слабая защита от spurious wakeup и логики с несколькими уведомлениями.

// Неправильно
synchronized(lock) {
    if (!ready) {
        lock.wait();
    }
    // предполагается ready == true, но может быть false
}

// Правильно
synchronized(lock) {
    while (!ready) {
        lock.wait();
    }
}
if: возможна ошибка при спонтанном пробуждении
while: условие перепроверяется, безопаснее

4) Использование notify() вместо notifyAll() в ситуациях с несколькими типами ожиданий - может привести к взаимной блокировке (deadlock), если пробуждается не тот поток.

5) Некорректные значения таймаута (отрицательные) при wait(long) / wait(long, int) - IllegalArgumentException.

Изменения и современные практики

Сам метод Object.wait() не претерпел значительных изменений в последних версиях Java и остается частью базового API без депрекации. Однако появились и активнее используются альтернативы:

  • java.util.concurrent и Lock/Condition для более явного управления блокировками;
  • LockSupport и структуры из java.util.concurrent для построения масштабируемых примитивов;
  • Проекты виртуальных потоков (Project Loom) меняют подход к конкурентности, делая модель потоков дешевле, что влияет на выбор инструментов, но не меняет поведение wait/notify.

Резюме: Object.wait() сохраняет прежнюю специфику; при новой разработке рекомендуется рассматривать высокоуровневые структуры или Lock/Condition в зависимости от задач.

Расширенные и редкие сценарии применения

1) Буфер с фиксированным размером (producer-consumer) с использованием wait/notifyAll.

Пример java
import java.util.LinkedList;

public class BoundedBuffer {
    private final LinkedList q = new LinkedList<>();
    private final int capacity;

    public BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }

    public void put(T item) throws InterruptedException {
        synchronized (this) {
            while (q.size() == capacity) {
                wait();
            }
            q.addLast(item);
            notifyAll(); // уведомить потребителей
        }
    }

    public T take() throws InterruptedException {
        synchronized (this) {
            while (q.isEmpty()) {
                wait();
            }
            T item = q.removeFirst();
            notifyAll(); // уведомить производителей
            return item;
        }
    }
}

// Результат: корректная работа нескольких производителей и потребителей без busy-wait
(при запуске с несколькими потоками данные корректно передаются между потоками без утечек и проигнорированных уведомлений)

2) Реализация ожидания с таймаутом и повторными попытками (retry/backoff).

Пример java
synchronized (monitor) {
    long timeout = 5000; // мс
    long deadline = System.currentTimeMillis() + timeout;
    while (!conditionMet()) {
        long remaining = deadline - System.currentTimeMillis();
        if (remaining <= 0) {
            // таймаут
            break;
        }
        monitor.wait(remaining);
    }
}
(если условие не выполнено до истечения deadline - выполняется ветка таймаута)

3) Комбинация wait/notify с несколькими очередями ожидания (эмуляция Condition до появления Lock).

Пример java
// разделение логики уведомлений по типам событий
class MultiWait {
    private final Object lock = new Object();
    private boolean alphaReady = false;
    private boolean betaReady = false;

    void waitAlpha() throws InterruptedException {
        synchronized (lock) {
            while (!alphaReady) lock.wait();
            alphaReady = false;
        }
    }

    void signalAlpha() {
        synchronized (lock) {
            alphaReady = true;
            lock.notifyAll(); // пробуждаем всех, но код проверит alphaReady
        }
    }
}
(подход работает, но сложнее и менее эффективен по сравнению с Condition)

4) Использование wait как базовой primitive при реализации собственных синхронизаторов (например, простого Semaphore).

Пример java
class SimpleSemaphore {
    private int permits;
    public SimpleSemaphore(int permits) { this.permits = permits; }

    public synchronized void acquire() throws InterruptedException {
        while (permits == 0) wait();
        permits--;
    }

    public synchronized void release() {
        permits++;
        notify();
    }
}
(работает, но для продакшн-качества лучше использовать java.util.concurrent.Semaphore)

5) Обработка прерываний: аккуратная схема повторной установки флага прерывания.

Пример java
synchronized (lock) {
    boolean interrupted = false;
    try {
        while (!condition) {
            try {
                lock.wait();
            } catch (InterruptedException e) {
                interrupted = true; // не терять информацию
            }
        }
    } finally {
        if (interrupted) Thread.currentThread().interrupt();
    }
}
(прерывание обработано, но сохранен флаг прерывания для верхних уровней)

Пояснения: в расширенных сценариях wait/notify требует строгого соблюдения правил владения монитором и проверки условия в цикле. Для сложных задач предпочтительнее использовать Lock/Condition или готовые синхронизаторы.

джава Object.wait() function comments

En
Object.wait() Блокирует поток, пока не будет вызван notify()