В чем разница между использованием synchronized и ReentrantLock для синхронизации в Java?

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

Ответ

Оба механизма обеспечивают взаимное исключение (mutual exclusion) и повторный вход (reentrancy), но ReentrantLock предоставляет более расширенные возможности.

synchronized — встроенное в язык ключевое слово. Синхронизация осуществляется либо на методе (весь метод), либо на объекте в блоке кода.

ReentrantLock — класс из пакета java.util.concurrent.locks, реализующий интерфейс Lock. Он требует явной блокировки и разблокировки.

Сравнительная таблица: Критерий synchronized ReentrantLock
Получение блокировки Неявное, при входе в блок/метод. Явное, через вызов lock().
Освобождение блокировки Неявное, при выходе из блока/метода (даже при исключении). Явное, через unlock() (обязательно в finally!).
Попытка блокировки с таймаутом Нет. Есть: tryLock(long timeout, TimeUnit unit).
Прерываемое ожидание Нет. Поток, ожидающий блокировку, нельзя прервать. Есть: lockInterruptibly().
Честность (Fairness) Не гарантируется. Можно создать с политикой честности (new ReentrantLock(true)), что уменьшает голодание, но снижает пропускную способность.
Связка условий (Condition) Одно встроенное условие ожидания (wait(), notify()). Можно создать несколько объектов Condition на один lock для более точного управления потоками.
Гибкость Базовая, но лаконичная. Высокая, но требует аккуратного управления.

Пример synchronized:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++; // Критическая секция защищена
    }
    // Или блочная синхронизация:
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
}

Пример ReentrantLock с таймаутом и условием:

import java.util.concurrent.locks.*;

public class Buffer {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition(); // Условие "не полон"
    private final Condition notEmpty = lock.newCondition(); // Условие "не пуст"

    public void put(Object item) throws InterruptedException {
        lock.lock();
        try {
            while (bufferIsFull()) {
                notFull.await(); // Ждем, пока не освободится место
            }
            // ... добавить элемент ...
            notEmpty.signal(); // Сигнализируем, что буфер не пуст
        } finally {
            lock.unlock(); // Освобождение в finally ОБЯЗАТЕЛЬНО
        }
    }

    public Object take(long timeout, TimeUnit unit) throws InterruptedException {
        lock.lock();
        try {
            if (!lock.tryLock(timeout, unit)) { // Попытка получить lock с таймаутом
                throw new TimeoutException("Не удалось получить блокировку");
            }
            // ... взять элемент ...
            return item;
        } finally {
            lock.unlock();
        }
    }
}

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