Java ReentrantReadWriteLocks - как безопасно получить блокировку записи? - PullRequest
61 голосов
/ 21 января 2009

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

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

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

Ответы [ 12 ]

27 голосов
/ 21 января 2009

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

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

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

25 голосов
/ 29 января 2011

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

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

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 после снятия блокировки чтения и до получения блокировки записи. Таким образом, значение у не будет равно х.

Тестовый код: обновление блокировки чтения до блоков блокировки записи:

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>
21 голосов
/ 13 сентября 2013

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

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

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

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

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

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

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

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

8 голосов
/ 05 апреля 2010

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

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

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

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

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

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

4 голосов
/ 24 февраля 2015

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

Больше информации на http://www.javaspecialists.eu/archive/Issue215.html

4 голосов
/ 21 января 2009

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

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

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

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

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

2 голосов
/ 11 июля 2014

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

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();
        }
    }
}
1 голос
/ 03 декабря 2011

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

private volatile boolean alreadyLockedForReading = false;

public void lockForReading(Lock readLock){
   if(!alreadyLockedForReading){
      lock.getReadLock().lock();
   }
}
1 голос
/ 21 марта 2010

до ОП: просто разблокируйте столько раз, сколько вы ввели, просто так:

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 голос
/ 21 января 2009

Полагаю, 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);
  }
}
...