Синхронизировать доступ к данному ключу в ConcurrentMap

Я достаточно часто хочу получить доступ (и, возможно, добавить/удалить) элементы данного ConcurrentMap, чтобы только один поток мог получить доступ к любому отдельному ключу за раз. Как лучше всего это сделать? Синхронизация самого ключа не работает: другие потоки могут получить доступ к тому же ключу через экземпляр equal.

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


person Alexey Romanov    schedule 20.07.2011    source источник
comment
Действительно, мне интересно, почему у Карт нет метода getKey(key), который возвращает экземпляр ключа.   -  person Sergey Aslanov    schedule 20.07.2011
comment
Вы говорите, что хотите синхронизировать доступ в ConcurrentMap во время поиска (что не совсем понятно для меня) или постфактум, как только значение было получено с карты?   -  person Drew Noakes    schedule 20.07.2011
comment
@JustYo, если у вас есть экземпляр с равным ключом, зачем вам экземпляр на карте? Если бы этот ключ был изменяемым, я бы сомневался в выборе дизайна для использования этого ключа здесь.   -  person Thomas    schedule 20.07.2011
comment
@Thomas, имеющий такой метод, решит задачу синхронизации по ключу. Это не будет работать с равным, но другим экземпляром.   -  person Sergey Aslanov    schedule 20.07.2011
comment
Попробуйте использовать Hashtable   -  person    schedule 20.07.2011
comment
@JustYo Так зачем синхронизировать по ключу? Я могу представить себе 2 ситуации: 1. предотвратить одновременный доступ к значению -> синхронизировать значение. 2. значение может быть изменено/заменено, что приведет к ситуации, подобной грязному чтению -> добавьте уровень косвенности (например, держатель значения) и синхронизируйте его.   -  person Thomas    schedule 20.07.2011
comment
@Thomas Вариант 2 не поможет, если вы хотите удалить значение вместо его замены. Конечно, к держателю значения можно добавить пустое состояние...   -  person Alexey Romanov    schedule 20.07.2011


Ответы (3)


См. простое решение здесь Простые блокировки на основе имени Java?

РЕДАКТИРОВАТЬ: это решение имеет четкое отношение «происходит до» от разблокировки до блокировки. Однако следующее решение, которое сейчас отозвано, этого не делает. ConcurrentMap javadoc слишком легкий, чтобы гарантировать это.


(Изъято) Если вы хотите повторно использовать карту в качестве пула блокировок,

private final V LOCK = ...; // a fake value
// if a key is mapped to LOCK, that means the key is locked
ConcurrentMap<K,V> map = ...;

V lock(key)
    V value;  
    while( (value=map.putIfAbsent(key, LOCK))==LOCK )
        // another thread locked it before me
        wait();
    // now putIfAbsent() returns a real value, or null
    // and I just sucessfully put LOCK in it
    // I am now the lock owner of this key
    return value; // for caller to work on

// only the lock owner of the key should call this method
unlock(key, value)
    // I put a LOCK on the key to stall others
    // now I just need to swap it back with the real value
    if(value!=null) 
        map.put(key, value);
    else // map doesn't accept null value
        map.remove(key)
    notifyAll();

test()
    V value = lock(key);

    // work on value

    // unlock. 
    // we have a chance to specify a new value here for the next worker
    newValue = ...; // null if we want to remove the key from map
    unlock(key, newValue); // in finally{}

Это довольно беспорядочно, потому что мы повторно используем карту для двух разных целей. Лучше иметь пул блокировок как отдельную структуру данных, а карту оставить просто как хранилище k-v.

person irreputable    schedule 20.07.2011
comment
Я предпочитаю ответ sjr на связанный вопрос принятому. - person Alexey Romanov; 20.07.2011
comment
Почему? Конечно, значения должны быть слабыми, поэтому, если сейчас блокировка никому не нужна, записи будут удалены из карты блокировок. - person Alexey Romanov; 20.07.2011
comment
у вас все еще есть запись key-›weakReference на карте - person irreputable; 21.07.2011
comment
ключ или значение, присутствующие в карте, могут быть утилизированы сборщиком мусора. Если это произойдет, запись автоматически исчезнет с карты. Если у меня больше нет ссылки на ключ, у меня точно не будет ссылки на значение (доступ к которому осуществляется только через карта), и соответствующая запись будет удалена сборщиком мусора. - person Alexey Romanov; 21.07.2011
comment
Я понимаю. Я не был знаком с MapMaker. Но если объект блокировки может исчезнуть, а затем создаться заново, разные потоки могут synchronize работать с разными объектами, и отношение «до того, как произойдет», может не сохраниться. - person irreputable; 21.07.2011
comment
makeComputingMap гарантирует, что когда несколько потоков пытаются создать значение для ключа, которого нет в карте, создается только одно значение. И, конечно же, блокировка может исчезнуть только тогда, когда на ней никто не синхронизируется. - person Alexey Romanov; 21.07.2011
comment
Проблема возникает перед отношением от действия разблокировки к следующему действию блокировки. с synchronized(getLock(key)){...}, если возвращенная блокировка изменится, у нас будут проблемы с видимостью. Это побуждает меня пересмотреть свое решение с ConcurrentMap. Жаль, что javadoc этого просто не дает достаточных гарантий, чтобы моя разблокировка-блокировка имела отношение «произошло-прежде». Как и ConcurrentHashMap. Мы могли бы покопаться в реализации, чтобы поспорить о поведении синхронизации; но только на основе общедоступного javadoc мое решение не может гарантировать, что оно произойдет раньше, поэтому я тоже должен отозвать его. - person irreputable; 21.07.2011

    private static final Set<String> lockedKeys = new HashSet<>();

    private void lock(String key) throws InterruptedException {
        synchronized (lockedKeys) {
            while (!lockedKeys.add(key)) {
                lockedKeys.wait();
            }
        }
    }

    private void unlock(String key) {
        synchronized (lockedKeys) {
            lockedKeys.remove(key);
            lockedKeys.notifyAll();
        }
    }

    public void doSynchronouslyOnlyForEqualKeys(String key) throws InterruptedException {
        try {
            lock(key);

            //Put your code here.
            //For different keys it is executed in parallel.
            //For equal keys it is executed synchronously.

        } finally {
            unlock(key);
        }
    }
  • key может быть не только "String", но и любым классом с правильно переопределенными методами "equals" и "hashCode".
  • try-finally — очень важно — вы должны гарантировать разблокировку ожидающих потоков после вашей операции, даже если ваша операция вызвала исключение.
  • Это не сработает, если ваша серверная часть распределена между несколькими серверами/виртуальными машинами JVM.
person Anton Fil    schedule 13.02.2019

Разве вы не можете просто создать свой собственный класс, расширяющий concurrentmap. Переопределить метод get(Object key), чтобы он проверял, не был ли уже запрошенный ключевой объект «извлечен» другим потоком?

Вам также придется создать новый метод в concurrentmap, который «возвращает» элементы на карту, чтобы они снова были доступны для другого потока.

person Foumpie    schedule 20.07.2011