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

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

Ответ 1

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

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

Ответ 2

То, что вы хотите сделать, должно быть возможным. Проблема в том, что 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 (SELECT FOR UPDATE...). Более того, он фактически уменьшает concurrency, потому что никто не может читать или писать какие-либо данные во время выполнения транзакции!

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

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>

Ответ 3

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

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

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

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

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

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

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

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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

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

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

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

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

Ответ 7

А как насчет этого?

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

Ответ 8

Я полагаю, что 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);
  }
}

Ответ 9

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

Ответ 10

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

private volatile boolean alreadyLockedForReading = false;

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

Ответ 11

Найдено в документации для 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();
   }
 }

Ответ 12

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