В каких случаях в современной Java-разработке уместно использовать synchronized, wait и notifyAll?

«В каких случаях в современной Java-разработке уместно использовать synchronized, wait и notifyAll?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Хотя высокоуровневые конструкции из пакета java.util.concurrent (JUC) часто предпочтительнее, базовые механизмы synchronized, wait() и notifyAll() всё ещё имеют свои нишевые применения.

1. synchronized

Когда использовать: Для простейших случаев синхронизации доступа к общему ресурсу в рамках одного JVM, когда достаточно встроенной блокировки монитора.

public class SimpleCounter {
    private int count;
    // synchronized гарантирует атомарность инкремента
    public synchronized void increment() {
        count++;
    }
    public synchronized int getCount() {
        return count;
    }
}

Почему не всегда: synchronized — это непрерываемая блокировка. Для более сложных сценариев (таймауты, честность, раздельные условия ожидания) лучше использовать ReentrantLock из JUC.

2. wait() и notifyAll()

Когда использовать: Для реализации шаблона «Ожидание-Уведомление» (Wait-Notify) внутри пользовательских примитивов синхронизации или пулов, где нужно координировать потоки на основе изменения состояния.

Классический пример: Простой bounded buffer (очередь ограниченной ёмкости)

public class SimpleBlockingQueue<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int maxSize;

    public SimpleBlockingQueue(int maxSize) { this.maxSize = maxSize; }

    public synchronized void put(T item) throws InterruptedException {
        while (queue.size() == maxSize) {
            wait(); // Ждём, пока потребитель не освободит место
        }
        queue.add(item);
        notifyAll(); // Уведомляем ожидающих потребителей
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // Ждём, пока производитель не добавит элемент
        }
        T item = queue.remove();
        notifyAll(); // Уведомляем ожидающих производителей
        return item;
    }
}

Критически важные правила:

  1. Всегда вызывайте wait(), notify(), notifyAll() только внутри блока/метода synchronized (поток должен владеть монитором объекта).
  2. Всегда используйте wait() в цикле while, проверяющем условие. Пробуждение (notify) может быть ложным (spurious wakeup).
  3. Предпочитайте notifyAll() notify(), если нет строгих требований к производительности, чтобы разбудить все ожидающие потоки.

В 90% случаев лучше выбрать из JUC:

  • Вместо собственной реализации блокирующей очереди — LinkedBlockingQueue.
  • Для координации потоков — CountDownLatch, CyclicBarrier, Semaphore.
  • Для условного ожидания — Condition из ReentrantLock (более гибкий аналог wait/notify).

Вывод: synchronized, wait(), notifyAll() — это низкоуровневый фундамент. Их стоит использовать для простой синхронизации или при реализации специфичных структур данных, в остальных случаях выбирайте готовые, оптимизированные и более безопасные классы из java.util.concurrent.*.