В чём разница между synchronized и ReentrantLock?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
В чём разница между 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 | Удобнее, автоматическое управление |
| Нужен таймаут | ReentrantLock | synchronized не поддерживает |
| Нужна справедливость | ReentrantLock | synchronized не справедлив |
| Нужны Conditions | ReentrantLock | wait/notify неудобны |
| Нужна прерываемость | ReentrantLock | lockInterruptibly() |
| Критична производительность | Обычно 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