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


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

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

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

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

EDIT чтобы прояснить это, поскольку я не думаю, что я объяснил это слишком хорошо изначально - я знаю, что в этом классе нет встроенной эскалации блокировки, и что мне нужно просто освободить блокировку чтения и получить блокировку записи. Моя проблема/была в том, что независимо от того, что делают другие потоки, вызов getReadLock().unlock() на самом деле не может освободить этой удержание потока на замке, если он получил его повторно, и в этом случае вызов getWriteLock().lock() будут блокировать навсегда поскольку этот поток все еще удерживает блокировку чтения и, таким образом, блокирует себя.

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

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 была специально разработана для реентерации, наверняка есть какой-то разумный способ поднять блокировку чтения до блокировки записи, когда блокировки могут быть получены повторно? Это критическая часть, которая означает, что наивный подход не работает.

12 60

12 ответов:

Я сделал небольшой прогресс в этом. Объявив переменную блокировки явно как 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();
}

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

что вы хотите сделать, должно быть возможным. Проблема в том, что 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);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

EDIT:
Как заметил РАН Байрон, если ваша проблема-голодание (блокировки чтения устанавливаются и освобождаются все время, никогда не опускаясь до нуля), вы можете попробовать использовать справедливую очередь. Но ваш вопрос не звучал так, как будто это была ваша проблема?

EDIT 2:
Теперь я вижу вашу проблему, вы фактически приобрели несколько блокировок чтения в стеке, и вы хотели бы преобразовать их в блокировку записи (обновление). Это на самом деле невозможно с JDK-реализацией, так как она не отслеживает владельцев чтение-блокировка. Там могут быть другие, удерживающие блокировки чтения, которые вы не увидите, и он понятия не имеет, сколько из блокировок чтения принадлежит вашему потоку, не говоря уже о вашем текущем стеке вызовов (т. е. ваш цикл убивает все читать замки, а не только свой собственный, так что ваш замок записи не будет ждать каких-либо параллельных читателей, чтобы закончить, и вы будете в конечном итоге с беспорядком на ваших руках)

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

Java 8 теперь имеет java.util.concurrent.locks.StampedLock с помощью tryConvertToWriteLock(long) API

дополнительная информация на 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 мотивируется рекурсивным обходом дерева:

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 увеличит количество семафоров чтения только в том случае, если этот поток еще не внес свой вклад в readHoldCount? Это означает, что в отличие от простого поддержания ThreadLocal readholdCount типа int, он должен поддерживать ThreadLocal набор типа Integer (поддержание hasCode текущего потока). Если это нормально, я бы предложил (по крайней мере, на данный момент) не вызывать несколько вызовов чтения в одном классе, а вместо этого использовать флаг, чтобы проверить, получена ли блокировка чтения текущим объект или нет.

private volatile boolean alreadyLockedForReading = false;

public void lockForReading(Lock readLock){
   if(!alreadyLockedForReading){
      lock.getReadLock().lock();
   }
}

to 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 не должен использоваться за пределами debug / monitor / fast-fail detect.

найдено в документации для ReentrantReadWriteLock. Он ясно говорит, что потоки чтения никогда не будут успешными при попытке получить блокировку записи. То, что вы пытаетесь достичь, просто не поддерживается. Ты должны отпустите блокировку чтения до получения блокировки записи. Снижение еще возможно.

Reentrancy

эта блокировка позволяет как читателям, так и писателям повторно получить чтение или запись замки в стиле {@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();
   }
 }

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