Программирование JavaМногопоточностьJava-разработчик серверных приложений

При обновлении данных под ReentrantReadWriteLock как сохранить возможность чтения после записи, не открывая...

При обновлении данных под ReentrantReadWriteLock как сохранить возможность чтения после записи, не открывая окно между блокировками?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нужно выполнить понижение блокировки: удерживая write lock, сначала захватить read lock, затем освободить write lock. После этого поток продолжает работу с read lock, а другие читатели уже могут присоединиться.

Освобождать write lock до захвата read lock нельзя: между этими операциями другой поток может изменить данные.

Исторический контекст

ReentrantReadWriteLock предназначен для сценариев, где чтения преобладают над изменениями. Он позволяет нескольким потокам одновременно читать состояние, но требует исключительного доступа для записи.

При обновлении иногда нужно сразу перейти к чтению результата. Отдельное последовательное освобождение write lock и получение read lock создало бы промежуток без защиты, поэтому механизм поддерживает безопасное понижение эксклюзивной блокировки до разделяемой.

Постановка проблемы

Поток изменил данные под write lock и хочет продолжить работу с ними под read lock. Если сначала освободить write lock, другой писатель может изменить данные до того, как исходный поток получит read lock.

Если же поток, удерживая read lock, попытается получить write lock, возникнет опасная попытка повышения блокировки: текущий поток не может завершить чтение и освободить read lock, пока ожидает write lock, а write lock не может быть выдан из-за удерживаемого read lock.

Подробное решение

Правильный порядок таков: захватить write lock, изменить состояние, захватить read lock, освободить write lock и выполнять дальнейшую работу под read lock.

import java.util.concurrent.locks.ReentrantReadWriteLock; class State { private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); private int value; int updateAndRead() { rw.writeLock().lock(); try { value++; rw.readLock().lock(); } finally { rw.writeLock().unlock(); } try { return value; } finally { rw.readLock().unlock(); } } }

Пока write lock удерживается, другие читатели и писатели не проходят. После захвата read lock текущим потоком запись уже защищена read lock, поэтому write lock можно отпустить. Другие читатели смогут получить read lock, но новый писатель будет ждать освобождения всех read lock.

Понижение безопасно благодаря реентерабельности: поток, владеющий write lock, может получить read lock. Обратное преобразование автоматически не поддерживается, поэтому для изменения данных обычно нужно заранее определить границу записи или полностью отпустить read lock перед повторным захватом write lock.

Ситуация из практики

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

Вариант с последовательными блокировками проще, но между ними возникает окно, в котором другой писатель может изменить конфигурацию. Попытка держать read lock и затем получить write lock может привести к взаимной блокировке.

Выбранное решение — захватить write lock, заменить конфигурацию, получить read lock и только затем отпустить write lock. Это сохраняет целостность перехода и позволяет нескольким потокам читать уже обновлённую конфигурацию. Компромисс состоит в том, что поток дольше удерживает read lock, поэтому последующие записи будут ждать завершения его проверки.

Что кандидаты часто упускают

  1. Можно ли безопасно повысить read lock до write lock?

Нет, автоматического повышения нет. Если поток удерживает read lock и ждёт write lock, он сам не может освободить read lock до завершения ожидающей операции, а write lock блокируется всеми читателями. Для такого сценария нужно завершить чтение, освободить read lock и заново получить write lock, повторно проверив условие.

  1. Почему read lock захватывают до освобождения write lock, а не после?

Так устраняется промежуток между блокировками. Пока write lock удерживается, никто не может изменить состояние; после получения read lock поток сохраняет защищённый доступ к данным. Если поменять порядок, другой писатель может выполнить изменение до получения read lock исходным потоком.

  1. Что произойдёт, если между захватом read lock и освобождением write lock возникнет исключение?

Write lock должен освобождаться в finally, иначе остальные потоки могут навсегда остаться заблокированными. Если read lock уже захвачен, его также необходимо освобождать в отдельном finally; на практике границы этих двух этапов разделяют так, чтобы для каждого успешно захваченного lock существовал гарантированный симметричный unlock.