← Назад к вопросам

В чём разница между synchronized и ReentrantLock?

2.0 Middle🔥 161 комментариев
#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

В чём разница между synchronized и ReentrantLock?

synchronized и ReentrantLock — это два основных механизма синхронизации в Java. Хотя оба решают проблему многопоточного доступа, они существенно отличаются по функциональности, гибкости и производительности.

Сравнение по основным характеристикам

1. Синтаксис и простота использования

synchronized — встроенная в язык конструкция:

synchronized (lock) {
    // Автоматически захватывает и отпускает
    sharedData.modify();
}

// Или на методе
public synchronized void method() {
    // this является блокировкой
    sharedData.modify();
}

ReentrantLock — требует явного управления:

ReentrantLock lock = new ReentrantLock();
lock.lock();  // Явное захватывание
try {
    sharedData.modify();
} finally {
    lock.unlock();  // ОБЯЗАТЕЛЬНО вызвать!
}

По удобству synchronized выигрывает — управление автоматическое. Однако это же является и ограничением.

2. Reentrancy (повторный вход в блокировку)

Оба класса поддерживают reentrancy — поток может повторно захватить блокировку, которую он уже держит.

synchronized — встроенная reentrancy:

public synchronized void outer() {
    System.out.println("outer");
    inner();  // Может повторно захватить this
}

public synchronized void inner() {
    System.out.println("inner");
    // Не создаёт deadlock, так как это reentrancy
}

ReentrantLock — явная reentrancy:

ReentrantLock lock = new ReentrantLock();

void outer() {
    lock.lock();
    try {
        System.out.println("outer");
        inner();  // Может повторно захватить
    } finally {
        lock.unlock();
    }
}

void inner() {
    lock.lock();
    try {
        System.out.println("inner");
        // lock успешно захвачена (ReentrantLock!)
    } finally {
        lock.unlock();
    }
}

3. Timeout и прерывания

Здесь ReentrantLock намного гибче.

synchronized — нет контроля:

// Поток может зависнуть без возможности отмены
synchronized (lock) {
    // Если lock занята — ждём вечно
    // Нет способа установить таймаут или отменить
}

ReentrantLock — полный контроль:

ReentrantLock lock = new ReentrantLock();

// Попытка с таймаутом
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        // Выполнить если успели захватить
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("Не смогли захватить блокировку за 1 сек");
}

// Попытка без ожидания
if (lock.tryLock()) {
    try {
        // выполнить
    } finally {
        lock.unlock();
    }
}

// С прерыванием потока
try {
    lock.lockInterruptibly();  // Может быть прервана!
    try {
        // выполнить
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    System.out.println("Блокировка была прервана");
    Thread.currentThread().interrupt();
}

4. Справедливость (Fairness)

synchronized — никакой справедливости:

// Если несколько потоков ждут lock,
// произвольный поток её захватит
synchronized (lock) { }

ReentrantLock — может быть справедлив:

// Потоки захватывают в порядке FIFO
ReentrantLock fairLock = new ReentrantLock(true);  // fair = true

// Без справедливости (быстрее)
ReentrantLock unfairLock = new ReentrantLock(false);  // или по умолчанию

Справедливая блокировка медленнее, но гарантирует, что ни один поток не будет голодать.

5. Условные переменные (Conditions)

synchronized использует wait/notify:

synchronized (lock) {
    while (!condition) {
        lock.wait();  // Ждём уведомления
    }
    // выполнить
}

// В другом потоке
synchronized (lock) {
    // выполнить что-то
    lock.notifyAll();  // Пробудить все потоки
}

Проблемы:

  • wait/notify сложны в использовании
  • Можно случайно проснуться false-wakeup
  • notify пробуждает ВСЕ потоки (неэффективно)

ReentrantLock использует Condition:

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

// Поток ждёт
lock.lock();
try {
    while (!isReady()) {
        condition.await();  // Ждёт уведомления
    }
    // выполнить
} finally {
    lock.unlock();
}

// Другой поток уведомляет
lock.lock();
try {
    // выполнить что-то
    setReady(true);
    condition.signalAll();  // Пробудить все
    // или condition.signal(); для одного
} finally {
    lock.unlock();
}

Advantage: можно создать несколько Condition и управлять ими независимо:

ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();

// Работники ждут, пока очередь не пустеет
notEmpty.await();

// Производители ждут, пока очередь не полна
notFull.await();

6. Производительность

В современной Java (17+) производительность примерно одинакова благодаря optimizations JVM. Однако:

  • synchronized — обычно немного быстрее на неконтестованных блокировках (JVM может их оптимизировать)
  • ReentrantLock — быстрее на контестованных блокировках (когда много потоков конкурируют)

Практические примеры

Пример 1: Простой кеш с synchronized

public class SimpleCache {
    private final Map<String, String> cache = new HashMap<>();
    
    public synchronized void put(String key, String value) {
        cache.put(key, value);
    }
    
    public synchronized String get(String key) {
        return cache.get(key);
    }
}

Пример 2: Тот же кеш с ReentrantLock

public class LockCache {
    private final Map<String, String> cache = new HashMap<>();
    private final ReentrantLock lock = new ReentrantLock();
    
    public void put(String key, String value) {
        lock.lock();
        try {
            cache.put(key, value);
        } finally {
            lock.unlock();
        }
    }
    
    public String get(String key) {
        lock.lock();
        try {
            return cache.get(key);
        } finally {
            lock.unlock();
        }
    }
}

Пример 3: Отличие с таймаутом (только ReentrantLock)

public class TimeoutLock {
    private final ReentrantLock lock = new ReentrantLock();
    
    public boolean tryDoSomething() {
        if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                doWork();
                return true;
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("Не смогли захватить за 100мс");
            return false;
        }
    }
    
    // synchronized не может это сделать!
}

Правило выбора

СценарийВыборПричина
Простая синхронизацияsynchronizedУдобнее, автоматическое управление
Нужен таймаутReentrantLocksynchronized не поддерживает
Нужна справедливостьReentrantLocksynchronized не справедлив
Нужны ConditionsReentrantLockwait/notify неудобны
Нужна прерываемостьReentrantLocklockInterruptibly()
Критична производительностьОбычно ReentrantLockЛучше на контестации
Быстрая разработкаsynchronizedМеньше кода, нет try/finally

Современная рекомендация

В Java 17+ используйте virtual threads (потоки-демоны) вместе с synchronized:

// Вместо ReentrantLock для асинхрона
virtual Thread.ofVirtual()
    .name("worker-1")
    .start(() -> {
        synchronized (lock) {
            // работа
        }
    });

Виртуальные потоки очень дешёвые, и synchronized становится практически идеальным вариантом.

Резюме

  • synchronized — первый выбор для простых случаев
  • ReentrantLock — когда нужна гибкость (timeout, conditions, fairness)
  • В критичных местах вообще избегайте явной синхронизации — используйте ConcurrentHashMap, BlockingQueue и другие структуры из java.util.concurrent
В чём разница между synchronized и ReentrantLock? | PrepBro