Что такое ReentrantLock в Java и когда его используют?

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

Ответ

ReentrantLock — это реализация интерфейса Lock из пакета java.util.concurrent.locks, предоставляющая механизм взаимного исключения (mutex) с расширенными возможностями по сравнению с ключевым словом synchronized.

Основные преимущества перед synchronized:

  1. Гибкость получения блокировки: Возможность попытаться получить lock без блокировки (tryLock()), с таймаутом или прерываемым ожиданием (lockInterruptibly()).
  2. Честность (Fairness): Можно создать "честную" блокировку (конструктор с true), которая гарантирует предоставление lock потоку, ждущему дольше всех (FIFO). У synchronized нет такой гарантии.
  3. Раздельные условия ожидания: Возможность создать несколько объектов Condition для одного lock, что позволяет разным потокам ожидать разных событий.

Повторный вход (Reentrancy): Как и synchronized, ReentrantLock является реентерабельным: поток, уже владеющий lock, может захватить его снова, не блокируясь сам себя.

Базовый шаблон использования:

ReentrantLock lock = new ReentrantLock();
// ...
lock.lock(); // Блокирующее получение блокировки
try {
    // Критическая секция
    sharedResource.modify();
} finally {
    lock.unlock(); // ВСЕГДА освобождайте lock в finally-блоке!
}

Пример с tryLock() и таймаутом:

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        // Критическая секция
    } finally {
        lock.unlock();
    }
} else {
    // Не удалось получить lock за указанное время
    System.out.println("Could not acquire the lock");
}

Использование Condition:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

// Поток A: ожидание
lock.lock();
try {
    while (!someCondition) {
        condition.await(); // Освобождает lock и ждёт сигнала
    }
    // Действие при выполнении условия
} finally { lock.unlock(); }

// Поток B: сигнализация
lock.lock();
try {
    someCondition = true;
    condition.signalAll(); // Пробуждает все ожидающие потоки
} finally { lock.unlock(); }

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

  • Требуется таймаут или прерываемое ожидание блокировки.
  • Нужна честность (fair ordering) для предотвращения голодания потоков.
  • Требуются сложные схемы синхронизации с использованием нескольких условий (Condition).
  • Необходимо проверить, свободна ли блокировка (tryLock() без блокировки).

Когда достаточно synchronized:

  • В большинстве стандартных случаев простой синхронизации.
  • Когда важна простота кода и меньшая вероятность ошибок (например, забытый unlock).