Gymterview
middle

Назовите различия между synchronized и ReentrantLock

В Java 5 появился интерфейс Lock, а с ним — класс ReentrantLock, который предоставляет те же базовые возможности, что и ключевое слово synchronized, но дополняет их более тонким контролем над блокировкой.

Что такое реентрабельность (reentrant). И synchronized, и ReentrantLock являются реентрабельными: если поток уже владеет блокировкой и повторно пытается её захватить, ему это позволяется. Внутри ведётся счётчик захватов — блокировка освободится только когда счётчик вернётся к нулю. Например:

Пример
// synchronized — реентрабельность встроена
synchronized (lock) {
    synchronized (lock) { // Повторный захват того же монитора — ОК
        // работаем
    } // счётчик -1
} // счётчик = 0, блокировка освобождена

// ReentrantLock — то же поведение
ReentrantLock lock = new ReentrantLock();
lock.lock();   // счётчик = 1
lock.lock();   // счётчик = 2
lock.unlock(); // счётчик = 1
lock.unlock(); // счётчик = 0, блокировка освобождена

Сравнительная таблица:

Характеристика synchronized ReentrantLock
Синтаксис Ключевое слово языка Явный объект Lock
Освобождение Автоматическое при выходе из блока Ручное — обязательно в finally
Прерываемое ожидание Нет lockInterruptibly()
Попытка захвата без блокировки Нет tryLock()
Попытка захвата с таймаутом Нет tryLock(time, unit)
Политика честности (fairness) Нет (зависит от JVM) Да — new ReentrantLock(true)
Условия ожидания (Condition) Один набор (wait/notify) Множество Condition через newCondition()
Производительность при высокой конкуренции Хорошая (после оптимизаций JDK 6+) Часто лучше при множестве конкурирующих потоков
Совместимость с Virtual Threads Pinning — виртуальный поток «прикрепляется» к носителю Корректная работа — нет pinning

Базовый паттерн использования ReentrantLock:

Пример
Lock lock = new ReentrantLock();

lock.lock();
try {
    // обновление общего состояния
} finally {
    lock.unlock(); // ОБЯЗАТЕЛЬНО в finally
}

Попытка захвата с таймаутом:

Пример
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
    try {
        // критическая секция
    } finally {
        lock.unlock();
    }
} else {
    // не удалось захватить блокировку за 500 мс — альтернативная логика
}

Множественные Condition:

Пример
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();

// Производитель
lock.lock();
try {
    while (isFull()) notFull.await();
    addItem(item);
    notEmpty.signal();
} finally {
    lock.unlock();
}

С synchronized вы ограничены единственным набором wait()/notify() на объекте монитора, тогда как ReentrantLock позволяет создать несколько Condition (например, отдельно для «пусто» и «полно»), что делает код более выразительным и менее подверженным ошибкам.

Когда выбирать что:

  • synchronized — для простых сценариев, коротких критических секций, когда не нужна расширенная функциональность.
  • ReentrantLock — когда нужны таймауты, прерываемое ожидание, множественные условия, fairness-политика, или при использовании Virtual Threads (чтобы избежать pinning).

Аналогия: synchronized — это автоматическая дверь с замком: вошёл — замок защёлкнулся, вышел — открылся автоматически. ReentrantLock — это сейф с ключом: больше возможностей (можно попробовать открыть, можно подождать определённое время, можно прервать ожидание), но ответственность за то, чтобы закрыть сейф обратно, лежит на вас.

На собеседовании часто спрашивают: «Когда вы предпочтёте ReentrantLock вместо synchronized?» Ключевые аргументы: tryLock с таймаутом для предотвращения deadlock, множественные Condition для producer-consumer, и отсутствие pinning при работе с Virtual Threads в Java 21+.