Java ReentrantReadWriteLocks - как безопасно получить блокировку записи?

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

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

lock.getReadLock().unlock();
lock.getWriteLock().lock()

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

РЕДАКТИРОВАНИЕ для разъяснения этого поскольку я не думаю, что объяснил это слишком хорошо первоначально - я знаю, что нет никакого встроенного повышения уровня блокировок в этом классе, и что я должен просто выпустить блокировку чтения и получить блокировку записи. Моя проблема состоит в том, что независимо от того, что другие потоки делают, звоня getReadLock().unlock() может не на самом деле выпустить этот поток, держатся блокировка, если это получило его повторно используемо, в этом случае вызов к getWriteLock().lock() заблокируется навсегда, поскольку этот поток все еще имеет хранение над блокировкой чтения и таким образом блоками самой.

Например, этот фрагмент кода никогда не будет достигать println оператора, даже когда выполнено однопоточного без других потоков, получающих доступ к блокировке:

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");

Таким образом, я спрашиваю, там хорошая идиома для обработки этой ситуации? А именно, когда поток, который содержит блокировку чтения (возможно повторно используемо) обнаруживает, что должен делать некоторые записи и таким образом хочет "приостановить" свою собственную блокировку чтения для взятия блокировки записи (блокирующийся как требуется на других потоках для выпуска их, держит блокировку чтения), и затем "возьмите" держать чтение, привязывают то же состояние впоследствии?

Так как эта реализация ReadWriteLock была специально предназначена, чтобы быть повторно используемой, конечно, существует некоторый разумный способ поднять блокировку чтения до блокировки записи, когда блокировки могут быть получены повторно используемо? Это - критическая часть, которая означает, что наивный подход не работает.

63
задан Andrzej Doyle 22 January 2009 в 11:03
поделиться

6 ответов

Я сделал немного успехов на этом. Путем объявления переменной блокировки явно как ReentrantReadWriteLock вместо просто ReadWriteLock (меньше, чем идеал, но вероятно необходимое зло в этом случае) я могу звонить getReadHoldCount() метод. Это позволяет мне получить количество хранений для текущего потока, и таким образом я могу выпустить 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();
}

однако, будет этим лучшее, которое я могу сделать? Это не чувствует себя очень изящным, и я все еще надеюсь, что существует способ обработать это менее "ручным" способом.

27
ответ дан Andrii Abramov 7 November 2019 в 12:45
поделиться

То, что Вы ищете, является обновлением блокировки и не возможно (по крайней мере, не атомарно) использование стандартного java.concurrent ReentrantReadWriteLock. Ваш лучший выстрел, разблокируют/блокируют, и затем проверяют, что никто не сделал промежуток модификаций.

, Что Вы пытаетесь сделать, принуждение всех блокировок чтения из пути не является очень хорошей идеей. Читайте блокировки там по причине, которую Вы не должны писать.:)

РЕДАКТИРОВАНИЕ:
, Как Выполнил Biron, на которого указывают, если Вашей проблемой является исчерпание ресурсов (блокировки чтения устанавливаются и выпускаются все время, никогда не опускаясь до нуля) Вы могли попытаться использовать справедливую организацию очередей. Но Ваш вопрос не казался, что это было Вашей проблемой?

РЕДАКТИРОВАНИЕ 2:
я теперь вижу Вашу проблему, Вы на самом деле получили несколько блокировок чтения на стеке, и требуется преобразовать их в блокировку записи (обновление). Это на самом деле невозможно с реализацией JDK, поскольку она не отслеживает владельцев блокировки чтения. Могли быть другие, держащие блокировки чтения, которые Вы не будете видеть, и это понятия не имеет, сколько из блокировок чтения принадлежит Вашему потоку, не говоря уже о Вашем текущем стеке вызовов (т.е. Ваш цикл уничтожает весь блокировки чтения, не только Ваше собственное, таким образом, Ваша блокировка записи не будет ожидать никаких параллельных читателей для окончания, и Вы закончите с путаницей на Ваших руках)

, у меня на самом деле была подобная проблема, и я закончил тем, что писал свою собственную блокировку, отслеживающую того, кто получен что блокировки чтения и обновляющий их до блокировок записи. Хотя это было также видом Копии на записи блокировки чтения-записи (разрешающий одному писателю вдоль читателей), таким образом, это немного отличалось все еще.

4
ответ дан falstro 7 November 2019 в 12:45
поделиться

Я предполагаю эти 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);
  }
}
1
ответ дан Olivier 7 November 2019 в 12:45
поделиться

Используйте "справедливый" флаг на ReentrantReadWriteLock. "ярмарка" означает, что запросы блокировки подаются на прибывшем первом, сначала подаваемом. Вы могли испытать ограбление производительности с тех пор при издании запроса "записи" все последующие запросы "чтения" будут заблокированы, даже если они, возможно, были поданы, в то время как предсуществовавшие блокировки чтения все еще заблокированы.

-1
ответ дан Ran Biron 7 November 2019 в 12:45
поделиться

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

У вас не может быть блокировки чтения / записи, которую можно без проблем модернизировать с чтения на запись. Пример:

void test() {
    lock.readLock().lock();
    ...
    if ( ... ) {
        lock.writeLock.lock();
        ...
        lock.writeLock.unlock();
    }
    lock.readLock().unlock();
}

Теперь предположим, что в эту функцию войдут два потока. (И вы предполагаете, что параллелизм, не так ли? В противном случае вам не нужны блокировки в первую очередь ....)

Предположим, что оба потока будут запускаться одновременно и работать одинаково быстро. . Это означало бы, что оба получат блокировку чтения, что совершенно законно. Однако тогда оба в конечном итоге попытаются получить блокировку записи, которую НИ ОДИН из них никогда не получит: соответствующие другие потоки удерживают блокировку чтения!

Блокировки, которые позволяют модернизировать блокировки чтения до блокировок записи, по определению подвержены взаимоблокировкам. Извините, но вам нужно изменить свой подход.

8
ответ дан 24 November 2019 в 16:25
поделиться

в 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 не следует использовать, кроме отладки / отслеживания / быстрого обнаружения сбоев.

1
ответ дан 24 November 2019 в 16:25
поделиться
Другие вопросы по тегам:

Похожие вопросы: