В чем разница между использованием synchronized и ReentrantLock для синхронизации в 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, когда необходимы продвинутые функции: таймауты, прерываемые блокировки, несколько условий или честность.

Ответ 18+ 🔞

А, слушай, смотри, вот эти твои замки в Java... Ну, блядь, как же они меня иногда бесят, честное слово. Сидишь такой, думаешь: ну что за ёперный театр, synchronized или ReentrantLock? А оказывается, разница-то — овердохуища!

Короче, оба они, сука, одно и то же делают — не пускают других, пока один в домике. Mutual exclusion, повторный вход — всё это есть. Но один — как тупая дубина, встроенная в язык, а второй — как швейцарский нож с хуевой тучей функций, но если им неаккуратно помахать — себе же палец отрежешь.

synchronized — это типа, блядь, родная, мамина синхронизация. Написал ключевое слово — и всё, ты в бронежилете. Либо на весь метод нацепил, либо в блок кода засунул. Вышел из блока — замок сам упал. Даже если ты внутри, сука, исключение выкинул — всё равно отпустит. Просто, удобно, но туповато.

ReentrantLock — это уже не ключевое слово, а целый класс, надо руками всё делать. lock() позвал — unlock() в finally блоке не забудь, а то пипец, deadlock на раз-два получишь. Зато, блядь, какие возможности!

Вот смотри, табличку нарисовал, чтобы в голове не еблось:

Критерий synchronized ReentrantLock
Взял замок как? Сам взялся, когда в блок вошёл. Самому крикнуть lock(), явно.
Отпустил как? Сам отпустил, когда вышел. Самому орать unlock(), и обязательно в finally, а то всех залочишь нахуй!
Можно подождать немного? Не, нихуя. Будешь висеть, пока не дадут. Ага, tryLock с таймаутом есть. Не дали за время — пошёл нахуй, дальше делай что хочешь.
Можно прервать ожидание? Ни в жисть. Виси и терпи. Можно, lockInterruptibly() есть. Крикнули тебе interrupt() — ты проснулся и пошёл решать свои проблемы.
Честность есть? Какая, блядь, честность? Кто первый вскочил — того и тапки. Можно создать честный лок (new ReentrantLock(true)). Тогда кто первый в очередь встал — тот первый и зайдёт. Но это медленнее, ебать.
Условия (Condition) Одно, родненькое: wait() и notify(). А тут, сука, можно на один лок несколько разных Condition навешать! Как в отделениях больницы: одни ждут не пусто, другие — не полно. Красота!
Гибкость Как топор. Нарезал — и ладно. Как лазерный скальпель. Мощно, но если трясутся руки — себе же хуже сделаешь.

Вот synchronized, простой как три копейки:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        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 {
            // Пытаемся взять лок, но только N секунд. Не получилось — летит TimeoutException, и не висим тут, блядь, вечно.
            if (!lock.tryLock(timeout, unit)) {
                throw new TimeoutException("Не удалось получить блокировку, иди нахуй");
            }
            // ... берём элемент ...
            return item;
        } finally {
            lock.unlock();
        }
    }
}

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