Блокировки чтения и записи Java для ресурсов, сохраняющих память

В памяти находится большая коллекция объектов типа R. Для изменения объекта требуется блокировка записи, а для чтения - блокировка чтения. Я мог бы сохранить ReadWriteLock как закрытый член класса R, однако я хочу сохранить память. В любой момент только небольшой процент объектов изменяется или читается. Есть несколько способов отказаться от сохранения блокировки чтения и записи для определенного ресурса (например, если он не читался или не записывался в течение некоторого времени, t). Для целей этого вопроса предположим, что периодически может определяться, что блокировка ресурса может быть удалена. Однако имейте в виду, что пока блокировка ресурса удаляется в потоке, один или несколько других потоков могут попытаться изменить или прочитать ресурс. Все это происходит в многопоточной среде. Как бы вы реализовали это с наименьшим количеством блокировок?

Например, один из способов сделать это - сохранить блокировки чтения и записи в параллельной карте:

Map<R,ReadWriteLock> map = new ConcurrentHashMap<>();

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

Вы можете подумать, что можно использовать комбинацию computeifabsent и remove. Однако это не работает. Например:

//--Thread1 write lock--
ReadWriteLock rwl = map.computeIfAbsent(r, r -> new ReadWriteLock()); // 1
rwl.writeLock.lock();                                        // 4
//Modify r here

//--Thread2: Removing entry--
map.remove(r);                                               // 2

//Thread3: write lock
ReadWriteLock rwl = map.computeIfAbsent(r, r-> new ReadWriteLock()); // 3
rwl.writeLock.lock();                                        // 5
//Modify r here.

Проблема в том, что объект блокировки потоком 1 не будет таким же, как блокировка, полученная потоком 3 и неправильно разрешающая две записи одновременно. Цифры справа показывают порядок исполнения.

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

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


person dan b    schedule 04.09.2018    source источник
comment
Это домашнее задание или что-то подобное? Каков ваш настоящий вопрос? Вроде интересная проблема, но я не знаю, с чем у вас проблемы.   -  person Malte Hartwig    schedule 04.09.2018
comment
перед удалением R с карты дождитесь блокировки, пока она не уведомит.   -  person jaychang0917    schedule 04.09.2018
comment
@MalteHartwig Нет, не домашнее задание. В основном, как поддерживать блокировки чтения и записи для коллекции объектов без необходимости хранить блокировку чтения и записи для каждого объекта.   -  person dan b    schedule 04.09.2018
comment
@ jaychang0917 ReadWriteLocks не уведомляет о блокировке и разблокировке. Представьте код, если вы думаете, что у вас есть решение.   -  person dan b    schedule 04.09.2018
comment
Извините, это на самом деле так хорошо написано, что я так и подумал. Я думаю, что вопрос как таковой является слишком широким, возможно, вы могли бы показать свои усилия и указать, что конкретно не работает.   -  person Malte Hartwig    schedule 04.09.2018
comment
@MalteHartwig Ну, одно время я был профессором информатики :) Я добавил краткое изложение вопроса в конце. Также я уже продемонстрировал некоторые свои усилия в приведенном выше коде; используйте concurrenthashmap и putifabsent, удалите комбинацию.   -  person dan b    schedule 04.09.2018


Ответы (2)


Вы можете использовать методы compute и computeIfPresent в своих интересах. Важно выполнить добавление / блокировку / удаление внутри потребителей, чтобы сделать это атомарно.

Примечание. В своем примере вы использовали putIfAbsent, но он возвращает ранее присвоенное значение, а не новое назначенное значение.

public static class Locks<R>
{
    private ConcurrentHashMap<R, ReentrantReadWriteLock> locks = new ConcurrentHashMap<>();

    public void lock(R r, Function<ReentrantReadWriteLock, Lock> whichLock)
    {
        locks.compute(r, (key, lock) -> {
            ReentrantReadWriteLock actualLock = lock == null ? new ReentrantReadWriteLock() : lock;
            whichLock.apply(actualLock).lock();
            return actualLock;
        });
    }

    public void unlock(R r, Function<ReentrantReadWriteLock, Lock> whichLock)
    {
        locks.computeIfPresent(r, (key, lock) -> {
            whichLock.apply(lock).unlock();
            return lock; // you could return null here if lock is unlocked (see cleanUp) to remove it immediately
        });
    }

    public void cleanUp()
    {
        for (R r : new ArrayList<>(locks.keySet()))
        {
            locks.computeIfPresent(r, (key, lock) -> locks.get(r).isWriteLocked()
                                                     || locks.get(r).getReadLockCount() != 0 ? lock : null);
        }
    }
}

Обратите внимание, как я использую

  • compute в lock для создания новых блокировок и их немедленной блокировки.
  • computeIfPresent в unlock, чтобы проверить, есть ли вообще блокировка
  • computeIfPresent в cleanUp, чтобы проверить, нужна ли блокировка без блокировки записи другого потока, пока я проверяю счетчик блокировок чтения

Прямо сейчас unlock бесполезен (за исключением нулевых проверок, которые являются лишь мерой предосторожности). Возврат null в unlock хорошо очистит ненужные блокировки и сделает cleanUp устаревшим, но может увеличить потребность в создании новых блокировок. Это зависит от того, как часто используются замки.

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

person Malte Hartwig    schedule 04.09.2018
comment
Выглядит действительно хорошо. Я исправил проблему с отсутствием в моем коде адреса. - person dan b; 04.09.2018
comment
Почему вы говорите, что разблокировка бесполезна? - person dan b; 05.09.2018
comment
В документации java говорится, что не следует использовать isWriteLocked и getReadLockCount для управления синхронизацией (возможно, это не то, что здесь делается), но это похоже на отличное использование этих методов. - person dan b; 05.09.2018
comment
Это решение приводит к тривиальной тупиковой ситуации, когда два потока пытаются получить одну и ту же блокировку записи. Первый поток создает / получает блокировку. Затем второй поток пытается получить такую ​​же блокировку записи и блокирует метод вычисления CHM на Lock # lock, который блокирует метод разблокировки первого потока. - person John H; 05.09.2018
comment
@JohnH, ты абсолютно прав. Должен был знать, что это не так просто. Придется вернуться к этому позже. - person Malte Hartwig; 05.09.2018
comment
Не думаю, что метод разблокировки нужен. Вместо этого пусть методы блокировки возвращают блокировку и заставляют вызывающего абонента разблокировать блокировки. См. Предлагаемые мной правки выше. @MalteHartwig, если вы согласны, можете ли вы исправить приведенный выше код? - person dan b; 07.09.2018
comment
@MalteHartwig Между прочим, для очистки нет гарантии, что не может быть других вызовов чтения или записи, пока вызываются методы очистки. Но это нормально. Могут быть лучшие способы определить, когда пытаться очистить - person dan b; 07.09.2018

вопрос в том, как поддерживать блокировки чтения-записи для коллекции ресурсов без необходимости хранить блокировку чтения-записи для каждого объекта в коллекции и минимизировать конкуренцию за блокировку

Вы не думали об использовании полосатых замков? (например, https://google.github.io/guava/releases/19.0/api/docs/com/google/common/util/concurrent/Striped.html)

По сути, это набор из N блокировок для M данных, где N ‹M. Хеш-функция используется для сопоставления идентичности любого заданного элемента данных с блокировкой, которая его контролирует.

person besmirched    schedule 04.09.2018
comment
Это интересный подход, однако у него есть несколько недостатков. В основном я думаю, что это вызовет слишком сильную блокировку. Есть лучшее решение, представленное @MalteHartwig - person dan b; 04.09.2018
comment
Если посмотреть на это дальше, это может быть лучшим ответом. Думаю, я бы использовал слабую блокировку? - person dan b; 09.09.2018