Назовите различия между 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+.