Какие примитивы синхронизации существуют в Java?

«Какие примитивы синхронизации существуют в Java?» — вопрос из категории Java Core, который задают на 22% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В Java существует несколько примитивов синхронизации для управления многопоточностью, каждый решает конкретную задачу.

Основные блокировки и координация:

  1. synchronized — ключевое слово для создания мониторных блокировок методов или блоков кода.

    synchronized (lockObject) {
        // Критическая секция
    }

    Почему: Гарантирует атомарность и видимость изменений в рамках одного монитора.

  2. volatile — модификатор переменной, обеспечивающий видимость её изменений для всех потоков.

    private volatile boolean running;

    Почему: Запрещает кэширование переменной в регистрах потока, чтение всегда происходит из основной памяти.

  3. wait(), notify(), notifyAll() — методы класса Object для условной блокировки и пробуждения потоков.

    synchronized (obj) {
        while (!condition) {
            obj.wait(); // Освобождает монитор 'obj' и ждёт
        }
        // Действие
        obj.notifyAll(); // Пробуждает все ожидающие потоки
    }

    Почему: Позволяют потокам эффективно ждать выполнения определённого условия, освобождая блокировку.

Гибкие примитивы из java.util.concurrent:

  1. ReentrantLock — явная, повторно входимая блокировка с расширенными возможностями (честность, опрос, прерывание).

    Lock lock = new ReentrantLock();
    lock.lock();
    try {
        // Критическая секция
    } finally {
        lock.unlock(); // Важно освобождать в finally-блоке
    }

    Почему: Предоставляет больше контроля, чем synchronized (например, tryLock(), lockInterruptibly()).

  2. Semaphore — ограничивает количество потоков, одновременно получающих доступ к ресурсу.

    Semaphore sem = new Semaphore(5); // Максимум 5 потоков
    sem.acquire(); // Уменьшает счётчик (ждёт, если он 0)
    try {
        // Работа с ограниченным ресурсом
    } finally {
        sem.release(); // Увеличивает счётчик
    }

    Почему: Полезен для управления доступом к пулу ресурсов (например, подключениям к БД).

  3. CountDownLatch — одноразовый барьер, который заставляет потоки ждать завершения набора операций.

    CountDownLatch latch = new CountDownLatch(3);
    // В рабочих потоках:
    doWork();
    latch.countDown(); // Уменьшает счётчик
    // В главном потоке:
    latch.await(); // Блокируется, пока счётчик не станет 0

    Почему: Идеален для ожидания инициализации или старта нескольких сервисов.

  4. CyclicBarrier — многоразовый барьер, где потоки ждут друг друга в определённой точке.

    CyclicBarrier barrier = new CyclicBarrier(3);
    // В каждом из 3 потоков:
    doWorkPart();
    barrier.await(); // Все ждут здесь, пока не соберутся все 3
    continueWork();

    Почему: Полезен для параллельных алгоритмов, где этапы работы должны быть синхронизированы.

  5. Phaser — более гибкий и многоразовый барьер с динамической регистрацией участников. Почему: Позволяет потокам присоединяться и покидать барьер между фазами, что удобно для сложных сценариев.

Выбор примитива зависит от задачи:

  • synchronized/ReentrantLock — для эксклюзивного доступа к данным.
  • Semaphore — для ограничения параллелизма.
  • CountDownLatch — для однократного ожидания событий.
  • CyclicBarrier/Phaser — для синхронизации работы потоков по фазам.