Java ReentrantReadWriteLocks – как безопасно получить блокировку записи?
Я использую в своем коде в настоящий момент ReentrantReadWriteLock для синхронизации доступа к древовидной структуре. Эта структура велика и читается многими streamами одновременно со случайными изменениями в небольших ее частях – так что она, похоже, хорошо подходит к идиоме чтения-записи. Я понимаю, что с этим конкретным classом невозможно заблокировать блокировку чтения для блокировки записи, так как в Javadocs необходимо освободить блокировку чтения до получения блокировки записи. Раньше я использовал этот шаблон в не-реентерабельных контекстах.
Однако я нахожусь в том, что я не могу надежно получить блокировку записи без блокировки навсегда. Поскольку блокировка чтения является реентерабельной, и я фактически использую ее как таковой, простой код
lock.getReadLock().unlock(); lock.getWriteLock().lock()
может блокироваться, если я приобрел readlock повторно. Каждый вызов разблокировки просто уменьшает количество удержаний, и блокировка фактически освобождается только тогда, когда кол-во отсчетов достигает нуля.
- Многочисленные сканеры Java
- Общий метод в Java без общего аргумента
- Несколько строк заголовка JTable
- Проблема с отображением графического интерфейса AbstractTableModel
- Почему Gson fromJson бросает исключение JsonSyntaxException: ожидается какой-то тип, но какой-то другой тип?
EDIT, чтобы прояснить это, поскольку я не думаю, что я объяснил это слишком хорошо изначально – я знаю, что в этом classе нет встроенной эскалации блокировки, и что я должен просто освободить блокировку чтения и получить блокировку записи. Моя проблема заключается в том, что независимо от того, что делают другие streamи, вызов getReadLock().unlock()
не может фактически освободить getReadLock().unlock()
этого streamа в блокировке, если он приобрел ее повторно, и в этом случае вызов getWriteLock().lock()
будет блокироваться навсегда, поскольку этот stream по-прежнему удерживает блокировку чтения и, таким образом, блокирует себя.
Например, этот fragment кода никогда не достигнет инструкции println, даже если вы запускаете singlethreaded без каких-либо других streamов, обращающихся к блокировке:
final ReadWriteLock lock = new ReentrantReadWriteLock(); lock.getReadLock().lock(); // In real code we would go call other methods that end up calling back and // thus locking again lock.getReadLock().lock(); // Now we do some stuff and realise we need to write so try to escalate the // lock as per the Javadocs and the above description lock.getReadLock().unlock(); // Does not actually release the lock lock.getWriteLock().lock(); // Blocks as some thread (this one!) holds read lock System.out.println("Will never get here");
Поэтому я спрашиваю, есть ли приятная идиома, чтобы справиться с этой ситуацией? В частности, когда stream, содержащий блокировку чтения (возможно, повторно), обнаруживает, что ему нужно выполнить некоторую запись, и, таким образом, хочет «приостановить» свою собственную блокировку чтения, чтобы получить блокировку записи (блокировка, как требуется для других streamов, чтобы отпустить свои трюки на блокировку чтения), а затем «забрать» свою фиксацию на замок чтения в том же состоянии после этого?
Поскольку эта реализация ReadWriteLock была специально разработана, чтобы быть реентерабельной, наверняка есть какой-то разумный способ повысить блокировку чтения до блокировки записи, когда блокировки могут быть приобретены повторно? Это критическая часть, которая означает, что наивный подход не работает.
- Получение состояния JToggleButton
- Возможно ли сделать анонимные внутренние classы в Java static?
- Диапазон настройки для X, Y Axis-JfreeChart
- Как я могу «печатать» длительность в Java?
- Загружать данные массива в JTable
- Обязательный @QueryParam в JAX-RS (и что делать в их отсутствие)
- Сбросить буфер с помощью BufferedReader в Java?
- Как заставить пример SwingWorker работать правильно?
Я немного поработал над этим. ReentrantReadWriteLock
образом объявляя переменную блокировки как ReentrantReadWriteLock
вместо просто ReadWriteLock
(в данном случае это не идеальное, но, вероятно, необходимое зло), я могу вызвать метод getReadHoldCount()
. Это позволяет мне получить количество трюмов для текущего streamа, и, таким образом, я могу разблокировать readlock многократно (и снова получить его с таким же числом). Таким образом, это работает, как показано быстрым и грязным тестом:
final int holdCount = lock.getReadHoldCount(); for (int i = 0; i < holdCount; i++) { lock.readLock().unlock(); } lock.writeLock().lock(); try { // Perform modifications } finally { // Downgrade by reacquiring read lock before releasing write lock for (int i = 0; i < holdCount; i++) { lock.readLock().lock(); } lock.writeLock().unlock(); }
Тем не менее, это будет лучшее, что я могу сделать? Это не очень элегантно, и я все еще надеюсь, что есть способ справиться с этим менее «вручную».
То, что вы хотите сделать, должно быть возможным. Проблема в том, что Java не обеспечивает реализацию, которая может обновлять блокировки чтения для записи блокировок. В частности, javadoc ReentrantReadWriteLock говорит, что он не позволяет обновлять блокировку чтения для блокировки записи.
В любом случае, Якоб Дженков описывает, как его реализовать. Подробнее см. http://tutorials.jenkov.com/java-concurrency/read-write-locks.html#upgrade .
Почему необходимо обновлять считываемые записи
Модификация от чтения до записи блокируется (несмотря на утверждения об обратном в других ответах). Может возникнуть взаимоблокировка, и поэтому частью реализации является код для распознавания взаимоблокировок и их разбиения путем исключения исключения в stream для выхода из тупика. Это означает, что в рамках вашей транзакции вы должны обработать исключение DeadlockException, например, повторив работу. Типичная схема:
boolean repeat; do { repeat = false; try { readSomeStuff(); writeSomeStuff(); maybeReadSomeMoreStuff(); } catch (DeadlockException) { repeat = true; } } while (repeat);
Без этой возможности единственный способ реализовать сериализуемую транзакцию, которая последовательно считывает кучу данных, а затем записывать что-то на основе прочитанного, состоит в том, чтобы предвидеть, что письмо будет необходимо, прежде чем вы начнете, и, следовательно, получите блокировки WRITE для всех данных, которые прочитайте перед тем, как писать, что нужно написать. Это KLUDGE, который использует Oracle (SELECT FOR UPDATE …). Кроме того, это фактически уменьшает параллелизм, поскольку никто другой не может читать или писать какие-либо данные во время транзакции!
В частности, освобождение блокировки чтения до получения блокировки записи приведет к непоследовательным результатам. Рассматривать:
int x = someMethod(); y.writeLock().lock(); y.setValue(x); y.writeLock().unlock();
Вы должны знать, создает ли someMethod () или любой метод, который он вызывает, создает блокировку повторного входа на y! Предположим, вы это знаете. Затем, если вы сначала отпустите блокировку чтения:
int x = someMethod(); y.readLock().unlock(); // problem here! y.writeLock().lock(); y.setValue(x); y.writeLock().unlock();
другой stream может изменить y после того, как вы отпустите его блокировку чтения и до того, как вы получите блокировку записи на нем. Таким образом, значение y не будет равно x.
Тестовый код: обновление блокировки чтения блокировки записи:
import java.util.*; import java.util.concurrent.locks.*; public class UpgradeTest { public static void main(String[] args) { System.out.println("read to write test"); ReadWriteLock lock = new ReentrantReadWriteLock(); lock.readLock().lock(); // get our own read lock lock.writeLock().lock(); // upgrade to write lock System.out.println("passed"); } }
Вывод с использованием Java 1.6:
read to write test
Это старый вопрос, но вот как решение проблемы, так и некоторая справочная информация.
Как отмечали другие, classическая блокировка читателей-писателей (например, JDK ReentrantReadWriteLock ) по своей сути не поддерживает обновление блокировки чтения блокировки записи, поскольку это допускает тупик.
Если вам нужно безопасно приобрести блокировку записи, не выпустив сначала блокировку чтения, есть и более эффективная альтернатива: вместо этого взгляните на блокировку чтения-записи .
Я написал ReentrantReadWrite_Update_Lock и выпустил его как открытый источник под лицензией Apache 2.0. Я также разместил подробную информацию о подходе к списку рассылки параллельных запросов JSR166, и этот подход остался в курсе некоторых членов в этом списке.
Этот подход довольно прост, и, как я уже упоминал об интересе к параллелизму, идея не совсем новая, так как она обсуждалась в списке рассылки ядра Linux по крайней мере еще в 2000 году. Также ReaderWriterLockSlim платформы .NET поддерживает обновление блокировки также. Настолько эффективно, что эта концепция просто не была реализована на Java (AFAICT) до сих пор.
Идея состоит в том, чтобы обеспечить блокировку обновления в дополнение к блокировке чтения и блокировке записи . Блокировка обновления – это промежуточный тип блокировки между блокировкой чтения и блокировкой записи. Подобно блокировке записи, только один stream может получить блокировку обновления за раз. Но как блокировка чтения, он позволяет читать доступ к streamу, который удерживает его, и одновременно с другими streamами, которые содержат обычные блокировки чтения. Ключевой особенностью является то, что блокировка обновления может быть обновлена со своего статуса только для чтения, до блокировки записи, и это не поддается блокировке, потому что только один stream может удерживать блокировку обновления и быть в состоянии обновляться одновременно.
Это поддерживает модернизацию блокировки, и, кроме того, она более эффективна, чем обычная блокировка чтения-записи в приложениях с шаблонами доступа с чтением до записи , поскольку она блокирует чтение streamов в течение более коротких периодов времени.
Пример использования предоставляется на сайте . Библиотека имеет 100% -ный охват тестирования и находится в центре Maven.
То, что вы пытаетесь сделать, просто невозможно.
Вы не можете иметь блокировку чтения / записи, которую вы можете обновить от чтения до записи без проблем. Пример:
void test() { lock.readLock().lock(); ... if ( ... ) { lock.writeLock.lock(); ... lock.writeLock.unlock(); } lock.readLock().unlock(); }
Предположим, что в эту функцию войдут два streamа. (И вы предполагаете параллелизм, правильно? В противном случае вам не все равно, что блокировки в первую очередь ….)
Предположим, что оба streamа начнутся в одно и то же время и будут работать одинаково быстро . Это означало бы, что оба приобрели бы замок чтения, что вполне законно. Тем не менее, тогда оба в конечном итоге попытаются получить блокировку записи, из которых НИКОГДА их не будет: соответствующие другие streamи сохраняют блокировку чтения!
Замки, которые позволяют модернизировать блокировки чтения для блокировки записи, по определению являются склонными к взаимоблокировкам. Извините, но вам нужно изменить свой подход.
То, что вы ищете, – это обновление блокировки и невозможно (по крайней мере, не атомарно) с использованием стандартного java.concurrent ReentrantReadWriteLock. Ваш лучший снимок – разблокировка / блокировка, а затем проверьте, что никто не вносил изменений между ними.
То, что вы пытаетесь сделать, заставляя все блокировки чтения отключиться, не очень хорошая идея. Прочитайте блокировки по какой-то причине, что вы не должны писать. 🙂
РЕДАКТИРОВАТЬ:
Как заметил Ран Бирон, если ваша проблема – голод (читаемые блокировки устанавливаются и освобождаются все время, никогда не падая до нуля), вы можете попытаться использовать честную очередность. Но ваш вопрос звучал не так, как будто это была ваша проблема?
EDIT 2:
Теперь я вижу вашу проблему, вы на самом деле приобрели несколько стеков для чтения в стеке, и вы хотите преобразовать их в блокировку записи (обновление). Это на самом деле невозможно с реализацией JDK, поскольку он не отслеживает владельцев блокировки чтения. Могут существовать другие, у которых есть блокировки чтения, которые вы не видите, и он не знает, сколько из блоков read-lock принадлежит вашему streamу, не говоря уже о вашем текущем стеке вызовов (т. Е. Ваш цикл убивает все блокировки чтения, а не только ваш собственный, поэтому ваш блокиратор записи не будет ждать, пока кто-нибудь из ближайших читателей закончит работу, и вы получите беспорядок на armх)
У меня на самом деле была аналогичная проблема, и я закончил тем, что написал свой собственный замок, отслеживая, кто имеет то, что считывает-блокирует и модернизирует их, чтобы писать блокировки. Хотя это был также тип записи для записи / записи типа Copy-on-Write (позволяющий писать один писатель вместе с читателями), так что это было немного по-другому.
Java 8 теперь имеет java.util.concurrent.locks.StampedLock
с API tryConvertToWriteLock(long)
Дополнительная информация на http://www.javaspecialists.eu/archive/Issue215.html
А как насчет этого?
class CachedData { Object data; volatile boolean cacheValid; private class MyRWLock { private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public synchronized void getReadLock() { rwl.readLock().lock(); } public synchronized void upgradeToWriteLock() { rwl.readLock().unlock(); rwl.writeLock().lock(); } public synchronized void downgradeToReadLock() { rwl.writeLock().unlock(); rwl.readLock().lock(); } public synchronized void dropReadLock() { rwl.readLock().unlock(); } } private MyRWLock myRWLock = new MyRWLock(); void processCachedData() { myRWLock.getReadLock(); try { if (!cacheValid) { myRWLock.upgradeToWriteLock(); try { // Recheck state because another thread might have acquired write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } } finally { myRWLock.downgradeToReadLock(); } } use(data); } finally { myRWLock.dropReadLock(); } } }
Я полагаю, что ReentrantLock
мотивирован рекурсивным ReentrantLock
дерева:
public void doSomething(Node node) { // Acquire reentrant lock ... // Do something, possibly acquire write lock for (Node child : node.childs) { doSomething(child); } // Release reentrant lock }
Не можете ли вы реорганизовать свой код, чтобы переместить обработку блокировки вне рекурсии?
public void doSomething(Node node) { // Acquire NON-reentrant read lock recurseDoSomething(node); // Release NON-reentrant read lock } private void recurseDoSomething(Node node) { ... // Do something, possibly acquire write lock for (Node child : node.childs) { recurseDoSomething(child); } }
Итак, ожидаем, что java будет увеличивать число чтения семафора только в том случае, если этот stream еще не внес вклад в readHoldCount? Это означает, что в отличие от простого поддержания ThreadLocal readholdCount типа int, он должен поддерживать ThreadLocal Set типа Integer (поддерживая hasCode текущего streamа). Если это нормально, я бы предложил (по крайней мере пока) не вызывать несколько вызовов чтения в одном classе, а вместо этого использовать флаг для проверки того, что блокировка чтения уже получен текущим объектом или нет.
private volatile boolean alreadyLockedForReading = false; public void lockForReading(Lock readLock){ if(!alreadyLockedForReading){ lock.getReadLock().lock(); } }
к OP: просто разблокируйте столько раз, сколько вы вошли в замок, просто:
boolean needWrite = false; readLock.lock() try{ needWrite = checkState(); }finally{ readLock().unlock() } //the state is free to change right here, but not likely //see who has handled it under the write lock, if need be if (needWrite){ writeLock().lock(); try{ if (checkState()){//check again under the exclusive write lock //modify state } }finally{ writeLock.unlock() } }
в блокировке записи, так как любая параллельная программа для самостоятельного уважения проверяет состояние.
HoldCount не следует использовать после обнаружения отладки / мониторинга / быстрого отказа.
Найдено в документации для ReentrantReadWriteLock . В нем четко сказано, что нити считывателя никогда не удастся при попытке получить блокировку записи. То, что вы пытаетесь достичь, просто не поддерживается. Перед приобретением блокировки записи вы должны освободить блокировку чтения. Понижение ставок по-прежнему возможно.
Реентерабельность
Эта блокировка позволяет читателям и писателям повторно использовать блокировки чтения или записи в стиле {@link ReentrantLock}. Непреднамеренные читатели не допускаются до тех пор, пока не будут освобождены все блокировки записи, хранящиеся в streamе записи.
Кроме того, писатель может получить блокировку чтения, но не наоборот. Среди других приложений повторное размещение может быть полезно, когда блокировки записи сохраняются во время вызовов или обратных вызовов методам, выполняющим чтение с помощью блокировок чтения. Если читатель попытается получить блокировку записи, он никогда не преуспеет.
Использование примера из приведенного выше источника:
class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); } }
Используйте флаг «fair» на ReentrantReadWriteLock. «справедливое» означает, что запросы на блокировку подаются с первого раза, сначала подаются. Вы можете столкнуться с потерей производительности, поскольку, когда вы выберете запрос «написать», все последующие «прочитанные» запросы будут заблокированы, даже если бы они могли быть отправлены, пока существующие блокировки чтения все еще заблокированы.