JavaRush /Курсы /JAVA 25 SELF /ReentrantLock и ReadWriteLock: отличие, примеры

ReentrantLock и ReadWriteLock: отличие, примеры

JAVA 25 SELF
52 уровень , 2 лекция
Открыта

1. Класс ReentrantLock: гибкая блокировка

Ключевое слово synchronized отлично подходит для базовых случаев: быстро и просто можно защитить метод или блок кода. Но иногда хочется большего:

  • Явно управлять блокировкой (например, попытаться захватить её, а если не удалось — не ждать).
  • Разделять права на «чтение» и «запись» к ресурсу.
  • Прерывать ожидание блокировки.
  • Диагностировать, кто и когда захватил или освободил блокировку.

Для таких задач и придуманы классы ReentrantLock и ReadWriteLock. Они дают больше контроля и возможностей, чем старый добрый synchronized.

Что же это такое?

ReentrantLock — это класс, реализующий интерфейс Lock. Он работает примерно как synchronized, но с дополнительными «наворотами». Главное отличие — управление блокировкой становится явным: вы сами вызываете методы lock() и unlock().

Интересный момент — слово reentrant значит, что поток может захватить один и тот же замок несколько раз подряд, не создавая взаимной блокировки. Это полезно, если метод вызывает сам себя рекурсивно или работает с общей блокировкой в цепочке вызовов.

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

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int value = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // Захватываем блокировку
        try {
            value++;
        } finally {
            lock.unlock(); // Обязательно освобождаем блокировку!
        }
    }

    public int getValue() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}

Обратите внимание:
Вызов lock() и unlock() всегда должен сопровождаться конструкцией try...finally. Если забыть вызвать unlock(), то никакой другой поток не сможет войти в защищённый блок — получите «вечную пробку».

Возможности ReentrantLock

Попытка захвата:
Можно попробовать захватить блокировку, но не ждать бесконечно:

if (lock.tryLock()) {
    try {
        // Работаем
    } finally {
        lock.unlock();
    }
} else {
    // Не удалось захватить — делаем что-то другое
}

Ожидание с таймаутом:

if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    // Захватили за 100 мс
}

Проверка, захвачена ли блокировка:

if (lock.isLocked()) { ... }

Диагностика очереди ожидания, «справедливость» блокировки и прочие плюшки.

3. Пример: инкремент счётчика с ReentrantLock

Давайте развиваем наше консольное приложение (например, имитируем обработку заказов из разных потоков). Сравним, как выглядит работа с synchronized и с ReentrantLock.

Пример с synchronized

public class OrderCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

Аналогичный пример с ReentrantLock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OrderCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

В чём профит?

  • Можно попытаться захватить блокировку и не ждать бесконечно (tryLock()).
  • Можно реализовать сложную логику: например, захватывать несколько блокировок в определённом порядке (актуально для сложных структур данных).
  • Можно «разблокировать» из другого места (но делать это нужно очень аккуратно — всегда помнить про unlock()!).

4. ReadWriteLock: блокировка для чтения и записи

Что это такое?

ReadWriteLock — это не просто замок, а умный распределитель доступа. Его основная реализация — ReentrantReadWriteLock, и она делит блокировки на две категории: для чтения и для записи.

Когда потоки только читают данные и никто ничего не меняет, они могут спокойно работать вместе — чтение не мешает чтению. Но как только кто-то решает внести правку, все остальные должны подождать: запись допускает лишь одного участника и требует тишины.

Такой подход особенно полезен там, где чтений много, а изменений мало — например, в каталоге товаров, который пользователи постоянно просматривают, но обновляют лишь изредка.

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

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ProductCatalog {
    private final Map<String, String> products = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void addProduct(String id, String name) {
        rwLock.writeLock().lock();
        try {
            products.put(id, name);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public String getProduct(String id) {
        rwLock.readLock().lock();
        try {
            return products.get(id);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

Пример использования в нашем приложении

Допустим, у нас есть база заказов, которую читают все потоки (например, для поиска заказа), но время от времени поступают новые заказы (операция записи).

import java.util.*;
import java.util.concurrent.locks.*;

public class OrderDatabase {
    private final List<String> orders = new ArrayList<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // Добавление заказа (требует writeLock)
    public void addOrder(String order) {
        rwLock.writeLock().lock();
        try {
            orders.add(order);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // Получение копии всех заказов (можно читать параллельно)
    public List<String> getOrders() {
        rwLock.readLock().lock();
        try {
            // Возвращаем копию, чтобы не было гонки
            return new ArrayList<>(orders);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

Что происходит?

  • Пока никто не пишет, хоть тысяча потоков может одновременно читать список заказов.
  • Как только один поток начинает добавлять заказ — чтения блокируются, чтобы не получить «размазанные» данные.

5. Сравнение: когда что использовать?

Сценарий synchronized ReentrantLock ReadWriteLock
Простая синхронизация ✖ (избыточно)
Нужен таймаут/попытка захвата
Много операций чтения, мало записи ✔ (значительный прирост)
Необходима диагностика/статистика
Рекурсивная блокировка ✔ (реентрабельность)

Вывод:

  • Для простых случаев — используйте synchronized.
  • Для гибкости — ReentrantLock.
  • Для сценариев «читаем часто, пишем редко» — ReadWriteLock.

6. Визуализация: схема работы ReadWriteLock

flowchart LR
    subgraph Чтение
      T1[Поток 1] -- Чтение --> Orders
      T2[Поток 2] -- Чтение --> Orders
      T3[Поток 3] -- Чтение --> Orders
    end
    subgraph Запись
      T4[Поток 4] -- Запись (addOrder) --> Orders
    end
    Orders[Список заказов]
    style Orders fill:#f9f,stroke:#333,stroke-width:2px

Пока ни один поток не пишет, все могут читать одновременно. Как только появляется запись, остальные потоки ждут завершения writeLock.

7. Особенности реализации и нюансы

«Честность» (fairness)

У ReentrantLock и ReentrantReadWriteLock можно включить «честный» режим (fair mode): потоки обслуживаются в порядке очереди, а не «кто успел, тот и съел». Это предотвращает «голодание» потоков, но может снизить производительность.

Lock fairLock = new ReentrantLock(true); // true — честный режим
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);

Потенциальные ловушки

  • Забытый unlock: Если не вызвать unlock(), получите вечную блокировку. Всегда используйте try...finally.
  • Исключения внутри блокировки: Даже если в блоке кода случилось исключение, блокировка должна быть освобождена!
  • Избыточное использование ReadWriteLock: Для маленьких коллекций или если почти всегда только запись — смысла в ReadWriteLock мало, а код становится сложнее.

8. Типичные ошибки

Ошибка №1: забыли вызвать unlock()
Самая частая и коварная ошибка — забыть вызвать unlock() после захвата блокировки. В результате — вечная блокировка, потоки «висят». Всегда используйте try...finally, даже если кажется, что «тут ничего не сломается».

Ошибка №2: использовать ReadWriteLock там, где не нужно
Если у вас почти нет параллельных чтений, а запись — частая, ReadWriteLock только усложнит код и снизит производительность. Используйте его только там, где реально много одновременных читателей.

Ошибка №3: захватили несколько блокировок в разном порядке
Если ваш код захватывает несколько Lock (например, для нескольких объектов), всегда делайте это в одном и том же порядке во всех потоках. Иначе можно получить deadlock — потоки будут ждать друг друга вечно.

Ошибка №4: попытка заменить synchronized на ReentrantLock «просто так»
Не стоит бездумно менять все synchronized на Lock — это не всегда ускоряет программу и может сделать код менее читаемым.

Ошибка №5: забыли про reentrancy
Если один и тот же поток вызывает lock() несколько раз подряд — это нормально для ReentrantLock, но не забывайте, что unlock() надо вызвать столько же раз!

1
Задача
JAVA 25 SELF, 52 уровень, 2 лекция
Недоступна
Центральный учёт запасов склада 📦
Центральный учёт запасов склада 📦
1
Задача
JAVA 25 SELF, 52 уровень, 2 лекция
Недоступна
Реестр глобальных настроек приложения ⚙️
Реестр глобальных настроек приложения ⚙️
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Сергей Уровень 66
3 февраля 2026
Всем привет! У меня у одного задачи уже решенные? Ахха