Avatar ·

В чем разница между wait() и sleep() в Java? Когда что использовать?

Всем привет! Я только начал разбираться с многопоточностью в Java и запутался в двух методах: wait() и sleep(). Оба как бы "приостанавливают" поток, но ведут себя по-разному, и я не понимаю, когда что применять.

Вот пример, который я написал. Я хочу, чтобы один поток выводил числа, а второй — буквы, чередуясь. Но у меня получается каша:

public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(i);
                try { Thread.sleep(1000); } catch (InterruptedException e) {}
            }
        });

        Thread t2 = new Thread(() -> {
            for (char c = 'A'; c < 'F'; c++) {
                System.out.println(c);
                try { Thread.sleep(1000); } catch (InterruptedException e) {}
            }
        });

        t1.start();
        t2.start();
    }
}

Они просто выводят всё вперемешку. Я слышал, что wait() и notify() нужны для синхронизации, но не понимаю, как их прикрутить. В чём принципиальная разница между wait() и sleep()? И почему sleep() не решает мою задачу?

<<>>

Главное отличие: sleep() — пауза, wait() — освобождение монитора

Ключевое различие между sleep() и wait() в том, что sleep() просто приостанавливает текущий поток на заданное время, не отпуская блокировки (монитора), которые он удерживает. wait() же освобождает монитор и переводит поток в состояние ожидания, пока другой поток не вызовет notify() или notifyAll() на том же объекте.

Детальное сравнение

Характеристикаsleep()wait()
ПринадлежитКлассу Thread (статический)Классу Object (нестатический)
Освобождает монитор?НетДа
Требует синхронизации?НетДа (должен быть внутри synchronized блока)
ПробуждениеАвтоматически по таймеруПо notify()/notifyAll() или по таймеру (если есть аргумент)
НазначениеПауза в выполненииМежпоточная координация

Как исправить ваш пример с помощью wait/notify

Для чередования потоков нужно использовать общий объект-монитор и флаг, указывающий, чья очередь:

public class AlternatingExample {
    private static final Object lock = new Object();
    private static boolean isNumberTurn = true;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    while (!isNumberTurn) { // ждём, пока не наша очередь
                        try { lock.wait(); } catch (InterruptedException e) {}
                    }
                    System.out.println(i);
                    isNumberTurn = false;
                    lock.notify(); // будим другой поток
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                for (char c = 'A'; c < 'F'; c++) {
                    while (isNumberTurn) { // ждём, пока не наша очередь
                        try { lock.wait(); } catch (InterruptedException e) {}
                    }
                    System.out.println(c);
                    isNumberTurn = true;
                    lock.notify(); // будим другой поток
                }
            }
        });

        t1.start();
        t2.start();
    }
}

Вывод будет строго: 0, A, 1, B, 2, C, 3, D, 4, E.

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

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

Важные детали

  • sleep() можно вызвать из любого места, wait() — только внутри synchronized блока/метода.
  • После wait() поток просыпается и снова пытается захватить монитор — поэтому всегда используйте while для проверки условия (ложное пробуждение).
  • sleep() не сбрасывает состояние гонки — если поток уснул, удерживая блокировку, другие потоки не смогут её получить.
<<>>

Дополнение: практические сценарии и подводные камни

Коллега выше отлично объяснил теорию. Добавлю несколько практических моментов, которые часто сбивают новичков.

1. sleep() в synchronized блоке — это deadlock-ловушка

Если вы вызываете sleep() внутри synchronized блока, поток засыпает, не отпуская блокировку. Другие потоки, которым нужен этот монитор, будут бесконечно ждать. wait() таких проблем не создаёт.

// ПЛОХО: поток спит, блокируя других
synchronized(lock) {
    Thread.sleep(5000); // lock занят всё это время!
}

2. wait() можно вызвать с таймаутом

wait(1000) — поток проснётся либо по notify(), либо через 1 секунду. Это удобно, когда вы ждёте условие, но не хотите ждать вечно:

synchronized(lock) {
    while (!condition) {
        lock.wait(2000); // ждём максимум 2 секунды
        if (!condition) break; // таймаут — выходим
    }
}

3. Всегда проверяйте условие в цикле, а не в if

Поток может проснуться без notify() (spurious wakeup). Поэтому:

// НЕПРАВИЛЬНО:
if (!condition) lock.wait();

// ПРАВИЛЬНО:
while (!condition) lock.wait();

4. notify() vs notifyAll()

notify() будит один случайный поток, notifyAll() — все. Если у вас несколько потоков ждут разных условий, используйте notifyAll(), иначе можно получить deadlock.