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

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

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

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

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

ИЗМЕНИТЬ, чтобы прояснить это, поскольку я не думаю, что изначально слишком хорошо объяснил это - я знаю, что в этом классе нет встроенной эскалации блокировки, и что мне нужно просто отпустить прочитанное lock и получить блокировку записи. Моя проблема заключается в том, что независимо от того, что делают другие потоки, вызов 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 была специально разработана для повторной входимости, конечно, есть какой-нибудь разумный способ поднять блокировку чтения до блокировки записи, когда блокировки могут быть получены повторно? Это критическая часть, которая означает, что наивный подход не работает.


person Andrzej Doyle    schedule 21.01.2009    source источник
comment
Возникла ли ваша проблема из-за попытки установить неизвестное количество блокировок чтения в том же потоке, в котором вы выполняете запись? Не могли бы вы написать в другой ветке?   -  person justinhj    schedule 03.06.2009


Ответы (12)


Я немного продвинулся в этом вопросе. Явно объявив переменную блокировки как ReentrantReadWriteLock, а не просто ReadWriteLock (менее чем идеально, но, вероятно, в данном случае это неизбежное зло), я могу вызвать _ 3_. Это позволяет мне получить количество удержаний для текущего потока, и, таким образом, я могу освобождать блокировку чтения столько раз (и впоследствии повторно получать то же число). Итак, это работает, как показал быстрый и грязный тест:

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();
}

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

person Andrzej Doyle    schedule 21.01.2009
comment
Кроме того, я думаю, что getReadHoldCount возвращает счетчик для всех потоков (хотя в документе это четко не сказано). В то время как RRWL позволяет снимать блокировки чтения других потоков, ваш второй цикл for повторно захватывает их все с текущим потоком, что больше, чем вы хотите. - person Olivier; 21.01.2009
comment
Я думаю, вы смотрите на getRead LOCK Count * (), а не на getRead HOLD Count * (). Последний не имеет такого предупреждения в документации Javadocs и возвращает количество удержаний для текущего потока (например, блокировка дважды в потоке A, затем 3 раза в потоке B, он вернет 2 в потоке A, а не 5). - person Andrzej Doyle; 21.01.2009
comment
Точный! Это в значительной степени аннулирует мои два комментария :-) - person Olivier; 22.01.2009
comment
Классный раствор. Я отложу это в свою сумку-трюки на потом. - person Mike Clark; 05.11.2010
comment
Вы можете потерять консистенцию дерева. Блокировка чтения сделана для того, чтобы ничего не изменилось при работе с ней. И при этом вы пытаетесь своими руками изменить эту конструкцию. Так что, когда вы продолжите итерацию по дереву - это может быть совсем другое, и вам придется вернуться к началу итерации. - person porfirion; 07.04.2017

Это старый вопрос, но здесь есть и решение проблемы, и некоторая справочная информация.

Как отмечали другие, классическая блокировка читателей и писателей (например, JDK ReentrantReadWriteLock) по своей сути выполняет не поддерживает обновление блокировки чтения до блокировки записи, потому что это может привести к тупиковой ситуации.

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

Я написал ReentrantReadWrite_Update_Lock и выпустил его как открытый исходный код под лицензией Apache 2.0 здесь. Я также разместил подробную информацию о подходе к списку рассылки JSR166 по интересам параллелизма., и этот подход выдержал определенную проверку участниками из этого списка.

Подход довольно прост, и, как я уже говорил об интересе к параллелизму, идея не совсем нова, поскольку она обсуждалась в списке рассылки ядра Linux, по крайней мере, еще в 2000 году. Также платформа .Net ReaderWriterLockSlim также поддерживает обновление блокировки. Таким образом, эта концепция до сих пор просто не была реализована на Java (AFAICT).

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

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

Пример использования представлен на сайте. Библиотека имеет 100% тестовое покрытие и находится в центре Maven.

person npgall    schedule 13.09.2013
comment
Это все еще сохраняется? Я вижу, что ридми обновили, но код не менялся уже 2 года. - person Navin; 20.09.2015
comment
Я автор, и меня возмущают обвинения в том, что проект, который все еще работает, требует постоянного внимания :) Проект все еще поддерживается, но, если кто-то не регистрирует ошибку или запрос функции, больше ничего не нужно делать. Библиотека имеет полное тестовое покрытие, и разработка завершена. - person npgall; 22.09.2015
comment
Да ладно, редко можно увидеть полный проект OSS. Есть ли шанс, что это будет в стандартной библиотеке Java n + 1? Это похоже на то, что многие люди изобретают заново. - person Navin; 22.09.2015
comment
Да, я согласен. Я спросил в списке интересов параллелизма, можно ли в конечном итоге включить его в JDK. Но дискуссия сосредоточилась на других моментах, и никто не высказал своего мнения по этому поводу. Я бы тоже хотел, чтобы это было включено лично. - person npgall; 22.09.2015
comment
Это круто. Этот ответ должен был получить гораздо больше голосов, чем 12. Ну, 13 сейчас. - person Mike Nakis; 23.03.2017

То, что вы хотите делать, должно быть возможным. Проблема в том, что Java не предоставляет реализации, которая может модернизировать блокировки чтения до блокировок записи. В частности, в javadoc ReentrantReadWriteLock говорится, что он не позволяет перейти от блокировки чтения к блокировке записи.

Во всяком случае, Якоб Дженков описывает, как это реализовать. См. http://tutorials.jenkov.com/java-concurrency/read-write-locks.html#upgrade для получения дополнительной информации.

Почему требуется обновление блокировок чтения на запись

Обновление с блокировки чтения на запись допустимо (несмотря на утверждения об обратном в других ответах). Может возникнуть взаимоблокировка, и поэтому частью реализации является код для распознавания взаимоблокировок и их выхода, генерируя исключение в потоке для выхода из взаимоблокировки. Это означает, что в рамках транзакции вы должны обработать исключение DeadlockException, например, выполнив эту работу заново. Типичный образец:

boolean repeat;
do {
  repeat = false;
  try {
   readSomeStuff();
   writeSomeStuff();
   maybeReadSomeMoreStuff();
  } catch (DeadlockException) {
   repeat = true;
  }
} while (repeat);

Без этой возможности единственный способ реализовать сериализуемую транзакцию, которая последовательно читает кучу данных и затем записывает что-то на основе того, что было прочитано, - это предвидеть необходимость записи до того, как вы начнете, и, следовательно, получить блокировку WRITE для всех данных, которые прочтите перед тем, как писать, что нужно написать. Это KLUDGE, который использует Oracle (ВЫБРАТЬ ДЛЯ ОБНОВЛЕНИЯ ...). Более того, это фактически снижает параллелизм, потому что никто другой не может читать или записывать какие-либо данные во время выполнения транзакции!

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

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

другой поток может изменить 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
<blocks indefinitely>
person Bob    schedule 29.01.2011

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

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

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

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

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

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

person Steffen Heil    schedule 04.04.2010

В Java 8 теперь есть java.util.concurrent.locks.StampedLock с tryConvertToWriteLock(long) API.

Дополнительная информация на http://www.javaspecialists.eu/archive/Issue215.html

person qwerty    schedule 24.02.2015

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

То, что вы пытаетесь сделать, снятие всех блокировок чтения с пути - не очень хорошая идея. Блокировки чтения существуют по той причине, что вам не следует писать. :)

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

ИЗМЕНИТЬ 2:
Теперь я вижу вашу проблему, вы фактически приобрели несколько блокировок чтения в стеке и хотите преобразовать их в блокировку записи (обновление). На самом деле это невозможно с реализацией JDK, поскольку она не отслеживает владельцев блокировки чтения. Могут быть другие, удерживающие блокировки чтения, которых вы не увидите, и он не знает, сколько блокировок чтения принадлежит вашему потоку, не говоря уже о вашем текущем стеке вызовов (т.е. ваш цикл убивает все блокировки чтения, а не только ваши собственные, поэтому ваша блокировка записи не будет ждать, пока закончатся одновременные чтения, и вы получите беспорядок в ваших руках)

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

person falstro    schedule 21.01.2009
comment
Хорошо, вот в чем проблема. Я не пытаюсь отменить ВСЕ блокировки чтения, только те, которые удерживаются текущим потоком. Я рад, что блокировка записи будет ждать, пока другие потоки освободят свои блокировки чтения. - person Andrzej Doyle; 21.01.2009
comment
Фактически, реализация JDK в некоторой степени отслеживает, кто заблокировал блокировку чтения; getReadHoldCount () возвращает счетчик только для текущего потока (через ThreadLocal), поэтому в этой ситуации может быть вызвано точное количество unlocks (). Код, который я ввел в свой ответ, действительно работает. - person Andrzej Doyle; 21.01.2009
comment
интересно, затем это было изменено в jdk1.6, jdk1.5 вообще не использует локальные переменные потока. - person falstro; 21.01.2009
comment
Невозможно обновить блокировку чтения до блокировки записи, которая гарантированно завершится успешно. Вариант разблокировки / блокировки, а затем проверки, что никто не вносил изменений между ними, у меня сработал. - person Seun Osewa; 13.01.2010

Что насчет этого как-то такого?

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();
        }
    }
}
person Henry HO    schedule 11.07.2014
comment
Это будет подвержено тупикам. Если один поток находится внутри upgradeToWriteLock (), а другой поток владеет блокировкой чтения, первый поток будет блокироваться в ожидании блокировки записи, но другой поток не сможет вызвать dropReadLock (), поскольку оба метода синхронизированы. - person TrogDor; 03.12.2015

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

person xss    schedule 21.03.2010

Я полагаю, что 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);
  }
}
person Olivier    schedule 21.01.2009
comment
Не легко. В этом есть элемент шаблона посетителя, означающий, что мы блокируем узел для чтения, а затем вызываем другие классы, логика которых может или не может выполнять обратный вызов, указывая на тот же узел. Кроме того (в более абстрактном смысле) зачем делать блокировку реентерабельной, если вы не можете использовать ее таким образом безопасно? :-) - person Andrzej Doyle; 21.01.2009
comment
Идея состоит в том, чтобы получить блокировку чтения на уровне вызова клиента. Другие ваши классы могут указывать на внутренний метод (вероятно, станут защищенными), зная, что они работают в контексте блокировки чтения. - person Olivier; 21.01.2009
comment
Да, я мог бы это сделать, но тогда эти методы не являются потокобезопасными по своей сути. Насколько это возможно, я не хочу зависеть от того, что клиенту необходимо получить блокировку с слегка некорректным (но часто явно правильным) поведением, если они этого не сделают. - person Andrzej Doyle; 22.01.2009

Итак, ожидаем ли мы, что Java будет увеличивать счетчик семафоров чтения только в том случае, если этот поток еще не внес свой вклад в readHoldCount? Это означает, что в отличие от простого поддержания ThreadLocal readholdCount типа int, он должен поддерживать ThreadLocal Set типа Integer (поддерживая hasCode текущего потока). Если это нормально, я бы предложил (по крайней мере, на данный момент) не вызывать несколько вызовов чтения в одном и том же классе, а вместо этого использовать флаг, чтобы проверить, получена ли уже блокировка чтения текущим объектом или нет.

private volatile boolean alreadyLockedForReading = false;

public void lockForReading(Lock readLock){
   if(!alreadyLockedForReading){
      lock.getReadLock().lock();
   }
}
person Vishal Angrish    schedule 02.12.2011

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

Реентерабельность

Эта блокировка позволяет как читателям, так и авторам повторно устанавливать блокировки чтения или записи в стиле {@link ReentrantLock}. Не реентерабельные считыватели не допускаются до тех пор, пока не будут сняты все блокировки записи, удерживаемые потоком записи.

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

Пример использования из указанного выше источника:

 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();
   }
 }
person phi    schedule 02.07.2014
comment
Как я уже упоминал в вопросе, I understand that with this particular class, one cannot elevate a read lock to a write lock, so as per the Javadocs one must release the read lock before obtaining the write lock. Меня беспокоило исключительно как надежно снять блокировку чтения, если она была получена реентерабельным способом (и, таким образом, rwl.readLock().unlock() должен называться в несколько раз больше, чем 1 - а сколько?). - person Andrzej Doyle; 02.07.2014

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

person Ran Biron    schedule 21.01.2009
comment
Не думаю, что вы понимаете мою проблему, голод здесь не проблема. Попробуйте это в одном потоке: ReentrantReadWriteLock lock = new ReentrantReadWriteLock (true); lock.readLock (). замок (); lock.readLock (). замок (); lock.readLock (). unlock (); lock.writeLock (). замок (); Он по-прежнему блокирует навсегда. - person Andrzej Doyle; 21.01.2009