ReadWriteLock: как пустить толпу читателей и не уронить базу



В прошлых постах мы разобрали, как ограничивать количество работающих потоков через Semaphore. Но там всем было все равно, что именно делает соседний поток.

Теперь спустимся на уровень ниже к данным. Представим объект в памяти или строку в базе данных. У нас есть два типа операций:
1. Чтение (много).
2. Запись (редко).

Классический synchronized блок работает просто: один поток зашел — остальные курят в сторонке. Если 100 пользователей одновременно читают профиль пользователя, при использовании обычного монитора они будут заходить в критическую секцию строго по одному. Это убивает производительность на ровном месте.

Нам нужен замок с разделяемым доступом. В Java для этого есть пара ReadWriteLock и его реализация ReentrantReadWriteLock.

Логика работы максимально простая:
* Множество читателей могут держать read-лок одновременно. Читаем параллельно, конфликтов нет.
* Один писатель может держать write-лок. При этом он должен дождаться, пока все текущие читатели закончат.
* Новые читатели не пускаются, если стоит очередь из писателей. Это важно, чтобы избежать ситуации голодания записи (writer starvation), когда постоянный поток читателей никогда не дает писателю получить эксклюзивный доступ.

Как это выглядит в воде:

private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int balance;

public void deposit(int amount) {
lock.writeLock().lock(); // Эксклюзивный доступ
try {
this.balance += amount;
} finally {
lock.writeLock().unlock();
}
}

public int getBalance() {
lock.readLock().lock(); // Разделяемый доступ
try {
return this.balance;
} finally {
lock.readLock().unlock();
}
}

Где это выстрелит, а где убьет память:

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

Но есть нюанс. Сам ReadWriteLock тоже потребляет ресурсы операционной системы. На очень коротких операциях оверхед от захвата и освобождения двух разных локов может быть выше, чем польза от параллельного чтения. Кроме того, каждый захват лока генерирует contention (конкуренцию) и может приводить к переключениям контекста ОС. Если чтение метода занимает наносекунды, обычный synchronized может оказаться быстрее.

И самое главное правило, которое может отличить вас от других кандидатов на собеседовании: этот паттерн абсолютно бесполезен для обычных коллекций вроде ArrayList или HashMap, потому что даже простое get() часто требует изменения внутренних структур (например, обновление модифицированного флага в HashMap или расширение массива в ArrayList). Для них уже придумали потокобезопасные аналоги из пакета java.util.concurrent: ConcurrentHashMap, CopyOnWriteArrayList и другие, внутри которых логика оптимизирована гораздо хитрее.

Что спросят на интервью:
1. Что будет, если читатель держит read-лок, а пришел писатель? Писатель ждет.
2. Что будет, если пришло 10 писателей? Они выстроятся в FIFO-очередь и зайдут строго по одному после всех текущих читателей.
3. Что такое lock downgrading? Это переход от write-лока к read-локу без потери владения (сначала берем запись, пишем данные, затем берем чтение, не отпуская запись, проверяем данные еще раз и только потом отпускаем запись). Обратный процесс (upgrading) невозможен и приведет к дедлоку.
Просмотры: 76
Открыть в MAX Сообщить о неточности

Репосты публикации

Публикация канала

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти